[diffusion] feat: enable spatial-shard vae decode across GPUs (#28071)

This commit is contained in:
Mick
2026-06-14 20:19:44 +08:00
committed by GitHub
parent d72314808f
commit 582bd23f71
25 changed files with 2767 additions and 1416 deletions
@@ -4,6 +4,7 @@
import argparse
import dataclasses
from dataclasses import dataclass, field
from functools import lru_cache
from typing import Any
import torch
@@ -11,6 +12,54 @@ import torch
from sglang.multimodal_gen.configs.models.base import ArchConfig, ModelConfig
from sglang.multimodal_gen.utils import StoreBoolean
AUTO_PARALLEL_DECODE_MODE = "auto"
SPATIAL_SHARD_PARALLEL_DECODE_MODES = ("spatial_shard", "spatial")
@lru_cache(maxsize=8)
def is_spatial_shard_parallel_decode_mode(mode: str) -> bool:
return mode in SPATIAL_SHARD_PARALLEL_DECODE_MODES
@lru_cache(maxsize=8)
def is_auto_parallel_decode_mode(mode: str) -> bool:
return mode == AUTO_PARALLEL_DECODE_MODE
@lru_cache(maxsize=128)
def _should_use_auto_spatial_shard_parallel_decode(
z_shape: tuple[int, ...],
world_size: int,
min_latent_elements_per_rank: int,
) -> bool:
if world_size <= 1 or z_shape[-2] < world_size:
return False
latent_elements_per_rank = (
z_shape[0] * z_shape[-3] * z_shape[-2] * z_shape[-1]
) // world_size
return latent_elements_per_rank >= min_latent_elements_per_rank
def should_use_spatial_shard_parallel_decode(
config: Any, z: torch.Tensor | None = None, world_size: int = 1
) -> bool:
if not config.use_parallel_decode:
return False
if is_spatial_shard_parallel_decode_mode(config.parallel_decode_mode):
return True
if not is_auto_parallel_decode_mode(config.parallel_decode_mode):
return False
if not config.auto_parallel_decode_prefers_spatial_shard():
return False
if z is None:
return True
return config.should_use_auto_spatial_shard_parallel_decode(z, world_size)
@dataclass
class VAEArchConfig(ArchConfig):
@@ -41,8 +90,9 @@ class VAEConfig(ModelConfig):
use_temporal_tiling: bool = True
use_parallel_tiling: bool = True
use_temporal_scaling_frames: bool = True
use_parallel_decode: bool = False
parallel_decode_mode: str = "tiled"
use_parallel_decode: bool = True
parallel_decode_mode: str = AUTO_PARALLEL_DECODE_MODE
auto_parallel_decode_min_latent_elements_per_rank: int = 4096
def __post_init__(self):
self.blend_num_frames = (
@@ -52,6 +102,18 @@ class VAEConfig(ModelConfig):
def post_init(self):
pass
def auto_parallel_decode_prefers_spatial_shard(self) -> bool:
return False
def should_use_auto_spatial_shard_parallel_decode(
self, z: torch.Tensor, world_size: int
) -> bool:
return _should_use_auto_spatial_shard_parallel_decode(
tuple(z.shape),
world_size,
self.auto_parallel_decode_min_latent_elements_per_rank,
)
@staticmethod
def add_cli_args(parser: Any, prefix: str = "vae-config") -> Any:
"""Add CLI arguments for VAEConfig fields"""
@@ -59,98 +121,98 @@ class VAEConfig(ModelConfig):
f"--{prefix}.load-encoder",
action=StoreBoolean,
dest=f"{prefix.replace('-', '_')}.load_encoder",
default=VAEConfig.load_encoder,
default=None,
help="Whether to load the VAE encoder",
)
parser.add_argument(
f"--{prefix}.load-decoder",
action=StoreBoolean,
dest=f"{prefix.replace('-', '_')}.load_decoder",
default=VAEConfig.load_decoder,
default=None,
help="Whether to load the VAE decoder",
)
parser.add_argument(
f"--{prefix}.tile-sample-min-height",
type=int,
dest=f"{prefix.replace('-', '_')}.tile_sample_min_height",
default=VAEConfig.tile_sample_min_height,
default=None,
help="Minimum height for VAE tile sampling",
)
parser.add_argument(
f"--{prefix}.tile-sample-min-width",
type=int,
dest=f"{prefix.replace('-', '_')}.tile_sample_min_width",
default=VAEConfig.tile_sample_min_width,
default=None,
help="Minimum width for VAE tile sampling",
)
parser.add_argument(
f"--{prefix}.tile-sample-min-num-frames",
type=int,
dest=f"{prefix.replace('-', '_')}.tile_sample_min_num_frames",
default=VAEConfig.tile_sample_min_num_frames,
default=None,
help="Minimum number of frames for VAE tile sampling",
)
parser.add_argument(
f"--{prefix}.tile-sample-stride-height",
type=int,
dest=f"{prefix.replace('-', '_')}.tile_sample_stride_height",
default=VAEConfig.tile_sample_stride_height,
default=None,
help="Stride height for VAE tile sampling",
)
parser.add_argument(
f"--{prefix}.tile-sample-stride-width",
type=int,
dest=f"{prefix.replace('-', '_')}.tile_sample_stride_width",
default=VAEConfig.tile_sample_stride_width,
default=None,
help="Stride width for VAE tile sampling",
)
parser.add_argument(
f"--{prefix}.tile-sample-stride-num-frames",
type=int,
dest=f"{prefix.replace('-', '_')}.tile_sample_stride_num_frames",
default=VAEConfig.tile_sample_stride_num_frames,
default=None,
help="Stride number of frames for VAE tile sampling",
)
parser.add_argument(
f"--{prefix}.blend-num-frames",
type=int,
dest=f"{prefix.replace('-', '_')}.blend_num_frames",
default=VAEConfig.blend_num_frames,
default=None,
help="Number of frames to blend for VAE tile sampling",
)
parser.add_argument(
f"--{prefix}.use-tiling",
action=StoreBoolean,
dest=f"{prefix.replace('-', '_')}.use_tiling",
default=VAEConfig.use_tiling,
default=None,
help="Whether to use tiling for VAE",
)
parser.add_argument(
f"--{prefix}.use-temporal-tiling",
action=StoreBoolean,
dest=f"{prefix.replace('-', '_')}.use_temporal_tiling",
default=VAEConfig.use_temporal_tiling,
default=None,
help="Whether to use temporal tiling for VAE",
)
parser.add_argument(
f"--{prefix}.use-parallel-tiling",
action=StoreBoolean,
dest=f"{prefix.replace('-', '_')}.use_parallel_tiling",
default=VAEConfig.use_parallel_tiling,
default=None,
help="Whether to use parallel tiling for VAE",
)
parser.add_argument(
f"--{prefix}.use-parallel-decode",
action=StoreBoolean,
dest=f"{prefix.replace('-', '_')}.use_parallel_decode",
default=VAEConfig.use_parallel_decode,
default=None,
help="Whether to use parallel decode for VAE",
)
parser.add_argument(
f"--{prefix}.parallel-decode-mode",
choices=("tiled", "patch", "auto"),
choices=("tiled", "patch", "spatial_shard", "spatial", "auto"),
dest=f"{prefix.replace('-', '_')}.parallel_decode_mode",
default=VAEConfig.parallel_decode_mode,
default=None,
help="Parallel decode mode for VAE",
)
@@ -20,3 +20,5 @@ class Hunyuan3DVAEConfig(VAEConfig):
subfolder: str = "hunyuan3d-dit-v2-0"
load_encoder: bool = False
load_decoder: bool = True
use_parallel_decode: bool = False
parallel_decode_mode: str = "tiled"
@@ -39,3 +39,7 @@ class HunyuanVAEArchConfig(VAEArchConfig):
@dataclass
class HunyuanVAEConfig(VAEConfig):
arch_config: VAEArchConfig = field(default_factory=HunyuanVAEArchConfig)
auto_parallel_decode_min_latent_elements_per_rank: int = 32768
def auto_parallel_decode_prefers_spatial_shard(self) -> bool:
return True
@@ -30,3 +30,5 @@ class LTXAudioVAEArchConfig(VAEArchConfig):
@dataclass
class LTXAudioVAEConfig(VAEConfig):
arch_config: LTXAudioVAEArchConfig = field(default_factory=LTXAudioVAEArchConfig)
use_parallel_decode: bool = False
parallel_decode_mode: str = "tiled"
@@ -62,3 +62,7 @@ class LTXVideoVAEArchConfig(VAEArchConfig):
@dataclass
class LTXVideoVAEConfig(VAEConfig):
arch_config: LTXVideoVAEArchConfig = field(default_factory=LTXVideoVAEArchConfig)
auto_parallel_decode_min_latent_elements_per_rank: int = 1024
def auto_parallel_decode_prefers_spatial_shard(self) -> bool:
return True
@@ -38,8 +38,6 @@ class QwenImageVAEConfig(VAEConfig):
use_temporal_tiling: bool = False
use_parallel_tiling: bool = False
use_parallel_decode: bool = False
def get_vae_scale_factor(self):
return 2 ** len(self.arch_config.temperal_downsample)
@@ -83,13 +83,15 @@ class WanVAEConfig(VAEConfig):
use_parallel_tiling: bool = False
use_parallel_encode: bool = True
use_parallel_decode: bool = True
def __post_init__(self):
self.blend_num_frames = (
self.tile_sample_min_num_frames - self.tile_sample_stride_num_frames
) * 2
def auto_parallel_decode_prefers_spatial_shard(self) -> bool:
return True
def get_vae_scale_factor(self):
# Wan VAE does not expose block_out_channels like SD-style VAEs.
# Its spatial downsample factor is explicitly defined by scale_factor_spatial.
@@ -66,6 +66,7 @@ _SP: SequenceParallelGroupCoordinator | None = None
_PP: PipelineGroupCoordinator | None = None
_CFG: GroupCoordinator | None = None
_DP: GroupCoordinator | None = None
_VAE_DECODE: GroupCoordinator | None = None
_DIT: ProcessGroup | None = None
_VAE: ProcessGroup | None = None
@@ -152,6 +153,7 @@ def init_parallel_group_coordinator(
"tensor",
"sequence",
"classifier_free_guidance",
"vae_decode",
], f"parallel_mode {parallel_mode} is not supported"
if parallel_mode == "pipeline":
return PipelineGroupCoordinator(
@@ -169,12 +171,13 @@ def init_parallel_group_coordinator(
**kwargs,
)
else:
# fallback to GroupCoordinator
return GroupCoordinator(
group_ranks=group_ranks,
local_rank=local_rank,
torch_distributed_backend=backend,
group_name="cfg_group",
group_name=(
"vae_decode_group" if parallel_mode == "vae_decode" else "cfg_group"
),
)
@@ -434,6 +437,15 @@ def initialize_model_parallel(
parallel_mode="tensor",
)
global _VAE_DECODE
assert _VAE_DECODE is None, "VAE decode parallel group is already initialized"
_VAE_DECODE = init_parallel_group_coordinator(
group_ranks=rank_generator.get_ranks("tp-sp-pp-cfg"),
local_rank=get_world_group().local_rank,
backend=backend,
parallel_mode="vae_decode",
)
if vae_parallel_size > 0:
init_vae_group(dit_parallel_size, vae_parallel_size, backend)
init_dit_group(dit_parallel_size, backend)
@@ -537,6 +549,7 @@ def model_parallel_is_initialized() -> bool:
and _SP is not None
and _PP is not None
and _TP is not None
and _VAE_DECODE is not None
)
@@ -813,11 +826,8 @@ def get_vae_parallel_rank() -> int:
def get_decode_parallel_group_coordinator() -> GroupCoordinator:
sp_group = get_sp_group()
cfg_group = get_cfg_group()
if sp_group.world_size == 1 and cfg_group.world_size > 1:
return cfg_group
return sp_group
assert _VAE_DECODE is not None, "VAE decode parallel group is not initialized"
return _VAE_DECODE
def get_decode_parallel_world_size() -> int:
@@ -858,9 +868,9 @@ def init_vae_group(
def destroy_model_parallel() -> None:
"""Set the groups to none and destroy them."""
global _TP, _SP, _DP, _CFG, _PP, _DIT, _VAE
global _TP, _SP, _DP, _CFG, _PP, _VAE_DECODE, _DIT, _VAE
for group in (_TP, _SP, _DP, _CFG, _PP):
for group in (_TP, _SP, _DP, _CFG, _PP, _VAE_DECODE):
if group is not None:
group.destroy()
@@ -868,4 +878,4 @@ def destroy_model_parallel() -> None:
if group is not None:
torch.distributed.destroy_process_group(group)
_TP, _SP, _DP, _CFG, _PP, _DIT, _VAE = (None,) * 7
_TP, _SP, _DP, _CFG, _PP, _VAE_DECODE, _DIT, _VAE = (None,) * 8
@@ -0,0 +1,717 @@
import contextvars
import math
from contextlib import contextmanager
import torch
import torch.distributed as dist
import torch.nn as nn
import torch.nn.functional as F
from sglang.multimodal_gen.runtime.distributed.parallel_state import (
get_decode_parallel_group_coordinator,
get_decode_parallel_rank,
get_decode_parallel_world_size,
)
from sglang.multimodal_gen.runtime.platforms import current_platform
_SPATIAL_PARALLEL_DECODE_DISABLED = contextvars.ContextVar(
"spatial_parallel_decode_disabled", default=False
)
@contextmanager
def disable_spatial_parallel_decode():
token = _SPATIAL_PARALLEL_DECODE_DISABLED.set(True)
try:
yield
finally:
_SPATIAL_PARALLEL_DECODE_DISABLED.reset(token)
def spatial_parallel_decode_disabled() -> bool:
return _SPATIAL_PARALLEL_DECODE_DISABLED.get()
def _tensor_pad(x: torch.Tensor, len_to_pad: int, dim: int = -2):
return torch.cat(
[
x,
torch.zeros(
*x.shape[:dim],
len_to_pad,
*x.shape[dim + 1 :],
dtype=x.dtype,
device=x.device,
),
],
dim=dim,
)
def _tensor_chunk(x: torch.Tensor, dim: int = -2, world_size: int = 1, rank: int = 0):
if x is None:
return x
if world_size <= 1:
return x
return torch.tensor_split(x, world_size, dim=dim)[rank].contiguous(
memory_format=_halo_memory_format(x)
)
def split_for_parallel_decode(
x: torch.Tensor, upsample_count: int, world_size: int, rank: int
):
return split_height_for_parallel_decode(
x,
expected_height=x.shape[-2] * (2**upsample_count),
world_size=world_size,
rank=rank,
)
def split_height_for_parallel_decode(
x: torch.Tensor, expected_height: int, world_size: int, rank: int
):
if spatial_parallel_decode_disabled():
return x, None
x = _tensor_chunk(x, dim=-2, world_size=world_size, rank=rank)
return x, expected_height
def _maybe_contiguous_for_sp_gather(x: torch.Tensor) -> torch.Tensor:
if (
x.dim() == 5
and hasattr(torch, "channels_last_3d")
and x.is_contiguous(memory_format=torch.channels_last_3d)
and not x.is_contiguous()
):
return x.contiguous()
if (
x.dim() == 4
and x.is_contiguous(memory_format=torch.channels_last)
and not x.is_contiguous()
):
return x.contiguous()
return x
def gather_and_trim_height(x: torch.Tensor, expected_height: int | None):
if spatial_parallel_decode_disabled():
return x
if expected_height is None:
return x
x, _ = gather_variable_height(x)
if x.shape[-2] != expected_height:
x = x[..., :expected_height, :].contiguous()
return x
def gather_height_for_global_op(x: torch.Tensor) -> torch.Tensor:
if spatial_parallel_decode_disabled():
return x
return gather_variable_height(x)[0]
def chunk_height_for_parallel_decode(x: torch.Tensor) -> torch.Tensor:
if spatial_parallel_decode_disabled():
return x
return _tensor_chunk(
x,
dim=-2,
world_size=get_decode_parallel_world_size(),
rank=get_decode_parallel_rank(),
)
def chunk_height_by_sizes(x: torch.Tensor, heights: list[int]) -> torch.Tensor:
if spatial_parallel_decode_disabled():
return x
rank = get_decode_parallel_rank()
start = sum(heights[:rank])
return x[..., start : start + heights[rank], :].contiguous(
memory_format=_halo_memory_format(x)
)
def gather_height_sizes(x: torch.Tensor) -> list[int]:
"""gather heights of sharded feature_maps from peers"""
if spatial_parallel_decode_disabled():
return [x.shape[-2]]
world_size = get_decode_parallel_world_size()
if world_size <= 1:
return [x.shape[-2]]
local_height = torch.tensor([x.shape[-2]], device=x.device, dtype=torch.int64)
gathered = [torch.empty_like(local_height) for _ in range(world_size)]
dist.all_gather(
gathered,
local_height,
group=get_decode_parallel_group_coordinator().device_group,
)
return [int(height.item()) for height in gathered]
def gather_variable_height(x: torch.Tensor) -> tuple[torch.Tensor, list[int]]:
if spatial_parallel_decode_disabled():
return x, [x.shape[-2]]
world_size = get_decode_parallel_world_size()
if world_size <= 1:
return x, [x.shape[-2]]
heights = gather_height_sizes(x)
max_height = max(heights)
if x.shape[-2] < max_height:
x = _tensor_pad(x, max_height - x.shape[-2], dim=-2)
gathered = get_decode_parallel_group_coordinator().all_gather(
_maybe_contiguous_for_sp_gather(x), dim=-2
)
chunks = torch.split(gathered, max_height, dim=-2)
return (
torch.cat(
[chunk[..., :height, :] for chunk, height in zip(chunks, heights)], dim=-2
),
heights,
)
def _halo_memory_format(reference: torch.Tensor) -> torch.memory_format:
if reference.dim() > 1 and reference.stride(1) == 1:
if reference.dim() == 5 and hasattr(torch, "channels_last_3d"):
return torch.channels_last_3d
if reference.dim() == 4:
return torch.channels_last
return torch.contiguous_format
def _ensure_recv_buf(
recv_buf: torch.Tensor | None, reference: torch.Tensor
) -> torch.Tensor:
memory_format = _halo_memory_format(reference)
if (
recv_buf is None
or recv_buf.shape != reference.shape
or recv_buf.dtype != reference.dtype
or recv_buf.device != reference.device
or not recv_buf.is_contiguous(memory_format=memory_format)
):
return torch.empty(
reference.shape,
dtype=reference.dtype,
device=reference.device,
memory_format=memory_format,
)
return recv_buf
def halo_exchange(
x: torch.Tensor,
height_halo_size: int = 1,
recv_top_buf: torch.Tensor | None = None,
recv_bottom_buf: torch.Tensor | None = None,
height_pad_mode: str = "zeros",
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
"""exchange(send and recv) top/bottom conv-input halos with adjacent spatial ranks"""
if spatial_parallel_decode_disabled():
return x, recv_top_buf, recv_bottom_buf
if height_halo_size == 0:
return x, recv_top_buf, recv_bottom_buf
decode_group = get_decode_parallel_group_coordinator()
rank = get_decode_parallel_rank()
world_size = get_decode_parallel_world_size()
group = decode_group.device_group
group_ranks = decode_group.ranks
top_row_ref = x[..., :height_halo_size, :]
bottom_row_ref = x[..., -height_halo_size:, :]
recv_top_buf = _ensure_recv_buf(recv_top_buf, top_row_ref)
recv_bottom_buf = _ensure_recv_buf(recv_bottom_buf, bottom_row_ref)
p2p_ops = []
if rank > 0:
prev_rank = group_ranks[rank - 1]
top_row = top_row_ref.contiguous(memory_format=_halo_memory_format(top_row_ref))
p2p_ops.append(dist.P2POp(dist.irecv, recv_top_buf, prev_rank, group))
p2p_ops.append(dist.P2POp(dist.isend, top_row, prev_rank, group))
if rank < world_size - 1:
next_rank = group_ranks[rank + 1]
bottom_row = bottom_row_ref.contiguous(
memory_format=_halo_memory_format(bottom_row_ref)
)
p2p_ops.append(dist.P2POp(dist.isend, bottom_row, next_rank, group))
p2p_ops.append(dist.P2POp(dist.irecv, recv_bottom_buf, next_rank, group))
if p2p_ops:
reqs = dist.batch_isend_irecv(p2p_ops)
for req in reqs:
req.wait()
if rank == 0:
recv_top_buf.copy_(
_make_boundary_halo(
x,
recv_bottom_buf if world_size > 1 else None,
height_halo_size=height_halo_size,
is_top=True,
mode=height_pad_mode,
)
)
if rank == world_size - 1:
recv_bottom_buf.copy_(
_make_boundary_halo(
x,
recv_top_buf if world_size > 1 else None,
height_halo_size=height_halo_size,
is_top=False,
mode=height_pad_mode,
)
)
return (
torch.concat([recv_top_buf, x, recv_bottom_buf], dim=-2),
recv_top_buf,
recv_bottom_buf,
)
def _make_boundary_halo(
x: torch.Tensor,
neighbor: torch.Tensor | None,
*,
height_halo_size: int,
is_top: bool,
mode: str,
) -> torch.Tensor:
if mode == "zeros":
shape = list(x.shape)
shape[-2] = height_halo_size
return torch.zeros(shape, dtype=x.dtype, device=x.device)
if mode == "replicate":
edge = x[..., :1, :] if is_top else x[..., -1:, :]
return edge.expand(*edge.shape[:-2], height_halo_size, edge.shape[-1])
if mode == "reflect":
source = x
if is_top and neighbor is not None:
source = torch.cat([x, neighbor], dim=-2)
elif not is_top and neighbor is not None:
source = torch.cat([neighbor, x], dim=-2)
if is_top:
index = torch.arange(
height_halo_size, 0, -1, device=x.device, dtype=torch.long
)
else:
index = torch.arange(
source.shape[-2] - 2,
source.shape[-2] - 2 - height_halo_size,
-1,
device=x.device,
dtype=torch.long,
)
return source.index_select(-2, index)
raise ValueError(f"Unsupported spatial padding mode for parallel decode: {mode}")
def _pad_with_mode(
x: torch.Tensor, padding: tuple[int, ...], mode: str
) -> torch.Tensor:
if mode == "zeros":
return F.pad(x, padding)
return F.pad(x, padding, mode=mode)
def _set_conv_padding(module: nn.Module, padding: tuple[int, ...]) -> None:
module.padding = padding
module._reversed_padding_repeated_twice = tuple(
value for pad in reversed(padding) for value in (pad, pad)
)
def _conv_preserves_local_height(
*,
height_halo_size: int,
height_pad_top: int,
height_pad_bottom: int,
kernel_height: int,
dilation_height: int,
stride_height: int,
) -> bool:
kernel_span = dilation_height * (kernel_height - 1)
return (
stride_height == 1
and 2 * height_halo_size == kernel_span
and height_pad_top == height_halo_size
and height_pad_bottom == height_halo_size
)
def _conv3d_weight_is_channels_last_3d(weight: torch.Tensor) -> bool:
return (
weight.dim() == 5
and hasattr(torch, "channels_last_3d")
and (current_platform.is_cuda() or current_platform.is_rocm())
and weight.is_contiguous(memory_format=torch.channels_last_3d)
)
def _match_conv3d_input_format(x: torch.Tensor, weight: torch.Tensor) -> torch.Tensor:
if x.dim() == 5 and _conv3d_weight_is_channels_last_3d(weight):
return x.contiguous(memory_format=torch.channels_last_3d)
return x
def _spatial_parallel_conv_forward(
module: nn.Module,
x: torch.Tensor,
conv_forward,
*,
height_pad_mode: str,
match_conv3d_format: bool = False,
) -> torch.Tensor:
# send and recv halo
# x_padded: concatenated input
x_padded, module._halo_recv_top_buf, module._halo_recv_bottom_buf = halo_exchange(
x,
height_halo_size=module.height_halo_size,
recv_top_buf=module._halo_recv_top_buf,
recv_bottom_buf=module._halo_recv_bottom_buf,
height_pad_mode=height_pad_mode,
)
if match_conv3d_format:
x_padded = _match_conv3d_input_format(x_padded, module.weight)
if module.height_halo_size == 0:
return conv_forward(x_padded)
stride = module.stride[-2]
if _conv_preserves_local_height(
height_halo_size=module.height_halo_size,
height_pad_top=module.height_pad_top,
height_pad_bottom=module.height_pad_bottom,
kernel_height=module.kernel_size[-2],
dilation_height=module.dilation[-2],
stride_height=stride,
):
return conv_forward(x_padded)
heights = gather_height_sizes(x)
global_start = sum(heights[: module.rank])
global_height = sum(heights)
if stride > 1:
shift = (
global_start - module.height_halo_size + module.height_pad_top
) % stride
if shift:
x_padded = x_padded[..., shift:, :]
global_start += shift
if match_conv3d_format:
x_padded = _match_conv3d_input_format(x_padded, module.weight)
out = conv_forward(x_padded)
# trim the output to original shape
return _trim_conv_output_height(
out,
local_height=x.shape[-2],
global_height=global_height,
global_start=global_start,
height_halo_size=module.height_halo_size,
height_pad_top=module.height_pad_top,
height_pad_bottom=module.height_pad_bottom,
kernel_height=module.kernel_size[-2],
dilation_height=module.dilation[-2],
stride_height=stride,
)
class SpatialParallelConv2d(nn.Conv2d):
def __init__(
self,
in_channels: int,
out_channels: int,
kernel_size: int | tuple[int, int],
stride: int | tuple[int, int] = 1,
padding: int | tuple[int, int] = 0,
dilation: int | tuple[int, int] = 1,
groups: int = 1,
bias: bool = True,
padding_mode: str = "zeros",
height_padding: tuple[int, int] | None = None,
):
super().__init__(
in_channels=in_channels,
out_channels=out_channels,
kernel_size=kernel_size,
stride=stride,
padding=padding,
dilation=dilation,
groups=groups,
bias=bias,
padding_mode=padding_mode,
)
self.height_halo_size = (self.dilation[-2] * (self.kernel_size[-2] - 1)) // 2
if height_padding is None:
height_padding = (self.padding[-2], self.padding[-2])
self.height_pad_top, self.height_pad_bottom = height_padding
self.padding: tuple[int, int]
if self.height_halo_size > 0:
self._padding = (0, 0, 0, 0)
else:
self._padding = (0, 0, self.padding[0], self.padding[0])
_set_conv_padding(self, (0, self.padding[1]))
self._halo_recv_top_buf: torch.Tensor | None = None
self._halo_recv_bottom_buf: torch.Tensor | None = None
self.rank = get_decode_parallel_rank()
self.world_size = get_decode_parallel_world_size()
def forward(self, x):
if spatial_parallel_decode_disabled():
return self._direct_forward(x)
if any(self._padding):
x = _pad_with_mode(x, self._padding, self.padding_mode)
return _spatial_parallel_conv_forward(
self,
x,
super().forward,
height_pad_mode=self.padding_mode,
)
def _direct_forward(self, x):
width_pad = self.padding[-1]
padding = (
width_pad,
width_pad,
self.height_pad_top,
self.height_pad_bottom,
)
if any(padding):
x = _pad_with_mode(x, padding, self.padding_mode)
return F.conv2d(
x,
self.weight,
self.bias,
self.stride,
(0, 0),
self.dilation,
self.groups,
)
class SpatialParallelCausalConv3d(nn.Conv3d):
def __init__(
self,
in_channels: int,
out_channels: int,
kernel_size: int | tuple[int, int, int],
stride: int | tuple[int, int, int] = 1,
padding: int | tuple[int, int, int] = 0,
):
super().__init__(
in_channels=in_channels,
out_channels=out_channels,
kernel_size=kernel_size,
stride=stride,
padding=padding,
)
self.height_pad_top = self.padding[1]
self.height_pad_bottom = self.padding[1]
self.height_halo_size = (self.kernel_size[-2] - 1) // 2
self.padding: tuple[int, int, int]
if self.height_halo_size > 0:
self._padding = (
self.padding[2],
self.padding[2],
0,
0,
2 * self.padding[0],
0,
)
else:
self._padding = (
self.padding[2],
self.padding[2],
self.padding[1],
self.padding[1],
2 * self.padding[0],
0,
)
self.padding = (0, 0, 0)
self._halo_recv_top_buf: torch.Tensor | None = None
self._halo_recv_bottom_buf: torch.Tensor | None = None
self.rank = get_decode_parallel_rank()
self.world_size = get_decode_parallel_world_size()
def forward(self, x, cache_x=None):
padding = list(self._padding)
if spatial_parallel_decode_disabled():
padding[2] = self.height_pad_top
padding[3] = self.height_pad_bottom
if cache_x is not None and self._padding[4] > 0:
cache_x = cache_x.to(x.device)
x = torch.cat([cache_x, x], dim=2)
padding[4] -= cache_x.shape[2]
x = F.pad(x, padding)
x = x if current_platform.is_amp_supported() else x.to(self.weight.dtype)
if spatial_parallel_decode_disabled():
x = _match_conv3d_input_format(x, self.weight)
return F.conv3d(
x,
self.weight,
self.bias,
self.stride,
(0, 0, 0),
self.dilation,
self.groups,
)
return _spatial_parallel_conv_forward(
self,
x,
super().forward,
height_pad_mode="zeros",
match_conv3d_format=True,
)
class SpatialParallelConv3d(nn.Conv3d):
def __init__(
self,
in_channels: int,
out_channels: int,
kernel_size: int | tuple[int, int, int],
stride: int | tuple[int, int, int] = 1,
padding: int | tuple[int, int, int] = 0,
dilation: int | tuple[int, int, int] = 1,
groups: int = 1,
bias: bool = True,
padding_mode: str = "zeros",
height_padding: tuple[int, int] | None = None,
):
super().__init__(
in_channels=in_channels,
out_channels=out_channels,
kernel_size=kernel_size,
stride=stride,
padding=padding,
dilation=dilation,
groups=groups,
bias=bias,
padding_mode=padding_mode,
)
self.height_halo_size = (self.dilation[-2] * (self.kernel_size[-2] - 1)) // 2
if height_padding is None:
height_padding = (self.padding[-2], self.padding[-2])
self.height_pad_top, self.height_pad_bottom = height_padding
self.padding: tuple[int, int, int]
if self.height_halo_size > 0:
self._padding = (0, 0, 0, 0, 0, 0)
else:
self._padding = (
0,
0,
self.padding[1],
self.padding[1],
0,
0,
)
_set_conv_padding(self, (self.padding[0], 0, self.padding[2]))
self._halo_recv_top_buf: torch.Tensor | None = None
self._halo_recv_bottom_buf: torch.Tensor | None = None
self.rank = get_decode_parallel_rank()
self.world_size = get_decode_parallel_world_size()
def forward(self, x):
if spatial_parallel_decode_disabled():
return self._direct_forward(x)
if any(self._padding):
x = _pad_with_mode(x, self._padding, self.padding_mode)
return _spatial_parallel_conv_forward(
self,
x,
super().forward,
height_pad_mode=self.padding_mode,
match_conv3d_format=True,
)
def _direct_forward(self, x):
time_pad = self.padding[0]
width_pad = self.padding[-1]
padding = (
width_pad,
width_pad,
self.height_pad_top,
self.height_pad_bottom,
time_pad,
time_pad,
)
if any(padding):
x = _pad_with_mode(x, padding, self.padding_mode)
x = _match_conv3d_input_format(x, self.weight)
return F.conv3d(
x,
self.weight,
self.bias,
self.stride,
(0, 0, 0),
self.dilation,
self.groups,
)
class SpatialParallelZeroPad2d(nn.Module):
def __init__(self, padding: tuple[int, int, int, int]) -> None:
super().__init__()
self.padding = padding
self.rank = get_decode_parallel_rank()
self.world_size = get_decode_parallel_world_size()
def forward(self, x: torch.Tensor) -> torch.Tensor:
if spatial_parallel_decode_disabled():
return F.pad(x, self.padding)
left, right, top, bottom = self.padding
top = top if self.rank == 0 else 0
bottom = bottom if self.rank == self.world_size - 1 else 0
return F.pad(x, (left, right, top, bottom))
def _trim_conv_output_height(
out: torch.Tensor,
*,
local_height: int,
global_height: int,
global_start: int,
height_halo_size: int,
height_pad_top: int,
height_pad_bottom: int,
kernel_height: int,
dilation_height: int,
stride_height: int,
) -> torch.Tensor:
kernel_span = dilation_height * (kernel_height - 1)
min_i = math.ceil(
((-height_pad_top) - (global_start - height_halo_size)) / stride_height
)
max_i = math.floor(
(
(global_height - 1 + height_pad_bottom)
- kernel_span
- (global_start - height_halo_size)
)
/ stride_height
)
start = max(min_i, 0)
end = min(max_i + 1, out.shape[-2])
if start != 0 or end != out.shape[-2]:
out = out[..., start:end, :]
return out
@@ -25,6 +25,13 @@ from sglang.multimodal_gen.configs.models.vaes.flux import FluxVAEConfig
from sglang.multimodal_gen.runtime.managers.memory_managers.layerwise_offload import (
LayerwiseOffloadableModuleMixin,
)
from sglang.multimodal_gen.runtime.models.vaes.common import (
can_install_spatial_shard_parallel_decode,
)
from sglang.multimodal_gen.runtime.models.vaes.parallel.diffusers_spatial import (
enable_diffusers_decoder_spatial_parallel,
spatial_parallel_diffusers_decode,
)
class AutoencoderKL(nn.Module, LayerwiseOffloadableModuleMixin):
@@ -127,6 +134,15 @@ class AutoencoderKL(nn.Module, LayerwiseOffloadableModuleMixin):
self.use_slicing = False
self.use_tiling = False
self.use_parallel_decode = config.use_parallel_decode
self.parallel_decode_mode = config.parallel_decode_mode
self._spatial_parallel_decode_enabled = False
self._spatial_parallel_upsample_count = 0
if can_install_spatial_shard_parallel_decode(self.config):
self._spatial_parallel_upsample_count = (
enable_diffusers_decoder_spatial_parallel(self.decoder)
)
self._spatial_parallel_decode_enabled = True
# only relevant if vae tiling is enabled
self.tile_sample_min_size = sample_size
@@ -311,7 +327,12 @@ class AutoencoderKL(nn.Module, LayerwiseOffloadableModuleMixin):
if self.post_quant_conv is not None:
z = self.post_quant_conv(z)
dec = self.decoder(z)
if self._spatial_parallel_decode_enabled:
dec = spatial_parallel_diffusers_decode(
self.decoder, z, self._spatial_parallel_upsample_count
)
else:
dec = self.decoder(z)
if not return_dict:
return (dec,)
@@ -6,9 +6,23 @@ import torch
from torch import nn
from sglang.multimodal_gen.configs.models.vaes.sana import SanaVAEConfig
from sglang.multimodal_gen.runtime.distributed.parallel_state import (
get_decode_parallel_rank,
get_decode_parallel_world_size,
)
from sglang.multimodal_gen.runtime.layers.parallel_conv import (
gather_and_trim_height,
split_height_for_parallel_decode,
)
from sglang.multimodal_gen.runtime.managers.memory_managers.layerwise_offload import (
LayerwiseOffloadableModuleMixin,
)
from sglang.multimodal_gen.runtime.models.vaes.common import (
can_install_spatial_shard_parallel_decode,
)
from sglang.multimodal_gen.runtime.models.vaes.parallel.diffusers_spatial import (
enable_diffusers_decoder_spatial_parallel,
)
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
logger = init_logger(__name__)
@@ -25,6 +39,7 @@ class AutoencoderDC(nn.Module, LayerwiseOffloadableModuleMixin):
self._config = config
self._inner_model = None
self._loaded_state_dict: dict[str, torch.Tensor] = {}
self._spatial_parallel_decode_enabled = False
def _ensure_inner_model(self, state_dict: dict[str, torch.Tensor] | None = None):
if self._inner_model is not None:
@@ -72,6 +87,9 @@ class AutoencoderDC(nn.Module, LayerwiseOffloadableModuleMixin):
self._loaded_state_dict.clear()
self._inner_model = self._inner_model.to(device)
if can_install_spatial_shard_parallel_decode(self._config):
enable_diffusers_decoder_spatial_parallel(self._inner_model.decoder)
self._spatial_parallel_decode_enabled = True
@property
def config(self):
@@ -98,7 +116,24 @@ class AutoencoderDC(nn.Module, LayerwiseOffloadableModuleMixin):
def decode(self, z: torch.Tensor, **kwargs):
self._ensure_inner_model()
z = z.to(dtype=self.dtype)
return self._inner_model.decode(z, **kwargs)
if not self._spatial_parallel_decode_enabled:
return self._inner_model.decode(z, **kwargs)
expected_height = (
z.shape[-2] * self._config.arch_config.spatial_compression_ratio
)
z, expected_height = split_height_for_parallel_decode(
z,
expected_height=expected_height,
world_size=get_decode_parallel_world_size(),
rank=get_decode_parallel_rank(),
)
decoded = self._inner_model.decode(z, **kwargs)
if isinstance(decoded, tuple):
sample = gather_and_trim_height(decoded[0], expected_height)
return (sample, *decoded[1:])
sample = gather_and_trim_height(decoded.sample, expected_height)
return decoded.__class__(sample=sample)
def forward(self, x: torch.Tensor, **kwargs):
self._ensure_inner_model()
@@ -19,7 +19,14 @@ from diffusers.models.autoencoders.vae import (
from diffusers.models.modeling_outputs import AutoencoderKLOutput
from sglang.multimodal_gen.configs.models.vaes.flux import Flux2VAEConfig
from sglang.multimodal_gen.runtime.models.vaes.common import ParallelTiledVAE
from sglang.multimodal_gen.runtime.models.vaes.common import (
ParallelTiledVAE,
can_install_spatial_shard_parallel_decode,
)
from sglang.multimodal_gen.runtime.models.vaes.parallel.diffusers_spatial import (
enable_diffusers_decoder_spatial_parallel,
spatial_parallel_diffusers_decode,
)
class AutoencoderKLFlux2(ParallelTiledVAE):
@@ -110,6 +117,13 @@ class AutoencoderKLFlux2(ParallelTiledVAE):
self.use_slicing = False
self.use_tiling = False
self._spatial_parallel_decode_enabled = False
self._spatial_parallel_upsample_count = 0
if can_install_spatial_shard_parallel_decode(self.config):
self._spatial_parallel_upsample_count = (
enable_diffusers_decoder_spatial_parallel(self.decoder)
)
self._spatial_parallel_decode_enabled = True
# only relevant if vae tiling is enabled
self.tile_sample_min_size = self.config.sample_size
@@ -266,7 +280,12 @@ class AutoencoderKLFlux2(ParallelTiledVAE):
if self.post_quant_conv is not None:
z = self.post_quant_conv(z)
dec = self.decoder(z)
if self._spatial_parallel_decode_enabled:
dec = spatial_parallel_diffusers_decode(
self.decoder, z, self._spatial_parallel_upsample_count
)
else:
dec = self.decoder(z)
if not return_dict:
return (dec,)
@@ -3,6 +3,7 @@
from typing import Optional, Tuple, Union
import torch
import torch.distributed as dist
import torch.nn as nn
import torch.nn.functional as F
from diffusers.models.activations import get_activation
@@ -13,11 +14,28 @@ from diffusers.models.autoencoders.vae import (
from diffusers.models.modeling_outputs import AutoencoderKLOutput
from sglang.multimodal_gen.configs.models.vaes.qwenimage import QwenImageVAEConfig
from sglang.multimodal_gen.runtime.distributed import (
get_local_torch_device,
get_sp_world_size,
from sglang.multimodal_gen.runtime.distributed import get_local_torch_device
from sglang.multimodal_gen.runtime.distributed.parallel_state import (
get_decode_parallel_rank,
get_decode_parallel_world_size,
model_parallel_is_initialized,
)
from sglang.multimodal_gen.runtime.layers.parallel_conv import (
SpatialParallelCausalConv3d,
SpatialParallelConv2d,
SpatialParallelZeroPad2d,
chunk_height_for_parallel_decode,
disable_spatial_parallel_decode,
gather_and_trim_height,
gather_height_for_global_op,
split_for_parallel_decode,
)
from sglang.multimodal_gen.runtime.models.vaes.common import (
ParallelTiledVAE,
can_install_spatial_shard_parallel_decode,
has_decode_parallel_world,
should_run_spatial_shard_parallel_decode,
)
from sglang.multimodal_gen.runtime.models.vaes.common import ParallelTiledVAE
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
logger = init_logger(__name__) # pylint: disable=invalid-name
@@ -140,7 +158,14 @@ class QwenImageResample(nn.Module):
- 'downsample3d': 3D downsampling with zero-padding, convolution, and causal 3D convolution.
"""
def __init__(self, dim: int, mode: str) -> None:
def __init__(
self,
dim: int,
mode: str,
conv2d_cls: type[nn.Conv2d] = nn.Conv2d,
causal_conv3d_cls: type[nn.Conv3d] = QwenImageCausalConv3d,
zero_pad2d_cls: type[nn.Module] = nn.ZeroPad2d,
) -> None:
super().__init__()
self.dim = dim
self.mode = mode
@@ -149,26 +174,40 @@ class QwenImageResample(nn.Module):
if mode == "upsample2d":
self.resample = nn.Sequential(
QwenImageUpsample(scale_factor=(2.0, 2.0), mode="nearest-exact"),
nn.Conv2d(dim, dim // 2, 3, padding=1),
conv2d_cls(dim, dim // 2, 3, padding=1),
)
elif mode == "upsample3d":
self.resample = nn.Sequential(
QwenImageUpsample(scale_factor=(2.0, 2.0), mode="nearest-exact"),
nn.Conv2d(dim, dim // 2, 3, padding=1),
conv2d_cls(dim, dim // 2, 3, padding=1),
)
self.time_conv = QwenImageCausalConv3d(
self.time_conv = causal_conv3d_cls(
dim, dim * 2, (3, 1, 1), padding=(1, 0, 0)
)
elif mode == "downsample2d":
self.resample = nn.Sequential(
nn.ZeroPad2d((0, 1, 0, 1)), nn.Conv2d(dim, dim, 3, stride=(2, 2))
)
if conv2d_cls is SpatialParallelConv2d:
self.resample = nn.Sequential(
zero_pad2d_cls((0, 1, 0, 0)),
conv2d_cls(dim, dim, 3, stride=(2, 2), height_padding=(0, 1)),
)
else:
self.resample = nn.Sequential(
zero_pad2d_cls((0, 1, 0, 1)),
conv2d_cls(dim, dim, 3, stride=(2, 2)),
)
elif mode == "downsample3d":
self.resample = nn.Sequential(
nn.ZeroPad2d((0, 1, 0, 1)), nn.Conv2d(dim, dim, 3, stride=(2, 2))
)
self.time_conv = QwenImageCausalConv3d(
if conv2d_cls is SpatialParallelConv2d:
self.resample = nn.Sequential(
zero_pad2d_cls((0, 1, 0, 0)),
conv2d_cls(dim, dim, 3, stride=(2, 2), height_padding=(0, 1)),
)
else:
self.resample = nn.Sequential(
zero_pad2d_cls((0, 1, 0, 1)),
conv2d_cls(dim, dim, 3, stride=(2, 2)),
)
self.time_conv = causal_conv3d_cls(
dim, dim, (3, 1, 1), stride=(2, 1, 1), padding=(0, 0, 0)
)
@@ -257,6 +296,7 @@ class QwenImageResidualBlock(nn.Module):
out_dim: int,
dropout: float = 0.0,
non_linearity: str = "silu",
causal_conv3d_cls: type[nn.Conv3d] = QwenImageCausalConv3d,
) -> None:
super().__init__()
self.in_dim = in_dim
@@ -265,12 +305,17 @@ class QwenImageResidualBlock(nn.Module):
# layers
self.norm1 = QwenImageRMS_norm(in_dim, images=False)
self.conv1 = QwenImageCausalConv3d(in_dim, out_dim, 3, padding=1)
self.conv1 = causal_conv3d_cls(in_dim, out_dim, 3, padding=1)
self.norm2 = QwenImageRMS_norm(out_dim, images=False)
self.dropout = nn.Dropout(dropout)
self.conv2 = QwenImageCausalConv3d(out_dim, out_dim, 3, padding=1)
self.conv2 = causal_conv3d_cls(out_dim, out_dim, 3, padding=1)
shortcut_conv3d_cls = (
QwenImageCausalConv3d
if causal_conv3d_cls is SpatialParallelCausalConv3d
else causal_conv3d_cls
)
self.conv_shortcut = (
QwenImageCausalConv3d(in_dim, out_dim, 1)
shortcut_conv3d_cls(in_dim, out_dim, 1)
if in_dim != out_dim
else nn.Identity()
)
@@ -338,9 +383,10 @@ class QwenImageAttentionBlock(nn.Module):
dim (int): The number of channels in the input tensor.
"""
def __init__(self, dim):
def __init__(self, dim, spatial_parallel: bool = False):
super().__init__()
self.dim = dim
self.spatial_parallel = spatial_parallel
# layers
self.norm = QwenImageRMS_norm(dim)
@@ -348,6 +394,8 @@ class QwenImageAttentionBlock(nn.Module):
self.proj = nn.Conv2d(dim, dim, 1)
def forward(self, x):
if self.spatial_parallel:
x = gather_height_for_global_op(x).contiguous()
identity = x
batch_size, channels, time, height, width = x.size()
@@ -376,7 +424,10 @@ class QwenImageAttentionBlock(nn.Module):
x = x.view(batch_size, time, channels, height, width)
x = x.permute(0, 2, 1, 3, 4)
return x + identity
x = x + identity
if self.spatial_parallel:
x = chunk_height_for_parallel_decode(x)
return x
class QwenImageMidBlock(nn.Module):
@@ -395,16 +446,24 @@ class QwenImageMidBlock(nn.Module):
dropout: float = 0.0,
non_linearity: str = "silu",
num_layers: int = 1,
spatial_parallel: bool = False,
causal_conv3d_cls: type[nn.Conv3d] = QwenImageCausalConv3d,
):
super().__init__()
self.dim = dim
# Create the components
resnets = [QwenImageResidualBlock(dim, dim, dropout, non_linearity)]
resnets = [
QwenImageResidualBlock(dim, dim, dropout, non_linearity, causal_conv3d_cls)
]
attentions = []
for _ in range(num_layers):
attentions.append(QwenImageAttentionBlock(dim))
resnets.append(QwenImageResidualBlock(dim, dim, dropout, non_linearity))
attentions.append(QwenImageAttentionBlock(dim, spatial_parallel))
resnets.append(
QwenImageResidualBlock(
dim, dim, dropout, non_linearity, causal_conv3d_cls
)
)
self.attentions = nn.ModuleList(attentions)
self.resnets = nn.ModuleList(resnets)
@@ -452,6 +511,8 @@ class QwenImageEncoder3d(nn.Module):
input_channels: int = 3,
):
super().__init__()
dim_mult = list(dim_mult)
temperal_downsample = list(temperal_downsample)
# dim = config.arch_config.dim
# z_dim = config.arch_config.z_dim
# dim_mult = config.arch_config.dim_mult
@@ -577,6 +638,9 @@ class QwenImageUpBlock(nn.Module):
dropout: float = 0.0,
upsample_mode: Optional[str] = None,
non_linearity: str = "silu",
conv2d_cls: type[nn.Conv2d] = nn.Conv2d,
causal_conv3d_cls: type[nn.Conv3d] = QwenImageCausalConv3d,
zero_pad2d_cls: type[nn.Module] = nn.ZeroPad2d,
):
super().__init__()
self.in_dim = in_dim
@@ -588,7 +652,13 @@ class QwenImageUpBlock(nn.Module):
current_dim = in_dim
for _ in range(num_res_blocks + 1):
resnets.append(
QwenImageResidualBlock(current_dim, out_dim, dropout, non_linearity)
QwenImageResidualBlock(
current_dim,
out_dim,
dropout,
non_linearity,
causal_conv3d_cls,
)
)
current_dim = out_dim
@@ -598,7 +668,15 @@ class QwenImageUpBlock(nn.Module):
self.upsamplers = None
if upsample_mode is not None:
self.upsamplers = nn.ModuleList(
[QwenImageResample(out_dim, mode=upsample_mode)]
[
QwenImageResample(
out_dim,
mode=upsample_mode,
conv2d_cls=conv2d_cls,
causal_conv3d_cls=causal_conv3d_cls,
zero_pad2d_cls=zero_pad2d_cls,
)
]
)
self.gradient_checkpointing = False
@@ -655,8 +733,11 @@ class QwenImageDecoder3d(nn.Module):
dropout=0.0,
non_linearity: str = "silu",
input_channels=3,
use_parallel_decode: bool = False,
):
super().__init__()
dim_mult = list(dim_mult)
temperal_upsample = list(temperal_upsample)
self.dim = dim
self.z_dim = z_dim
self.dim_mult = dim_mult
@@ -665,20 +746,44 @@ class QwenImageDecoder3d(nn.Module):
self.temperal_upsample = temperal_upsample
self.nonlinearity = get_activation(non_linearity)
self.use_parallel_decode = use_parallel_decode
self.upsample_count = 0
self.world_size = 1
self.rank = 0
if dist.is_initialized() and model_parallel_is_initialized():
self.world_size = get_decode_parallel_world_size()
self.rank = get_decode_parallel_rank()
if use_parallel_decode and self.world_size > 1:
CausalConv3d = SpatialParallelCausalConv3d
Conv2d = SpatialParallelConv2d
ZeroPad2d = SpatialParallelZeroPad2d
spatial_parallel = True
else:
CausalConv3d = QwenImageCausalConv3d
Conv2d = nn.Conv2d
ZeroPad2d = nn.ZeroPad2d
spatial_parallel = False
# dimensions
dims = [dim * u for u in [dim_mult[-1]] + dim_mult[::-1]]
scale = 1.0 / 2 ** (len(dim_mult) - 2)
# init block
self.conv_in = QwenImageCausalConv3d(z_dim, dims[0], 3, padding=1)
self.conv_in = CausalConv3d(z_dim, dims[0], 3, padding=1)
# middle blocks
self.mid_block = QwenImageMidBlock(
dims[0], dropout, non_linearity, num_layers=1
dims[0],
dropout,
non_linearity,
num_layers=1,
spatial_parallel=spatial_parallel,
causal_conv3d_cls=CausalConv3d,
)
# upsample blocks
self.upsample_count = 0
self.up_blocks = nn.ModuleList([])
for i, (in_dim, out_dim) in enumerate(zip(dims[:-1], dims[1:])):
# residual (+attention) blocks
@@ -698,8 +803,13 @@ class QwenImageDecoder3d(nn.Module):
dropout=dropout,
upsample_mode=upsample_mode,
non_linearity=non_linearity,
conv2d_cls=Conv2d,
causal_conv3d_cls=CausalConv3d,
zero_pad2d_cls=ZeroPad2d,
)
self.up_blocks.append(up_block)
if upsample_mode is not None:
self.upsample_count += 1
# Update scale for next iteration
if upsample_mode is not None:
@@ -707,11 +817,17 @@ class QwenImageDecoder3d(nn.Module):
# output blocks
self.norm_out = QwenImageRMS_norm(out_dim, images=False)
self.conv_out = QwenImageCausalConv3d(out_dim, input_channels, 3, padding=1)
self.conv_out = CausalConv3d(out_dim, input_channels, 3, padding=1)
self.gradient_checkpointing = False
def forward(self, x, feat_cache=None, feat_idx=[0]):
expected_height = None
if self.use_parallel_decode and self.world_size > 1:
x, expected_height = split_for_parallel_decode(
x, self.upsample_count, self.world_size, self.rank
)
## conv1
if feat_cache is not None:
idx = feat_idx[0]
@@ -758,6 +874,8 @@ class QwenImageDecoder3d(nn.Module):
feat_idx[0] += 1
else:
x = self.conv_out(x)
if self.use_parallel_decode and self.world_size > 1:
x = gather_and_trim_height(x, expected_height)
return x
@@ -791,8 +909,12 @@ class AutoencoderKLQwenImage(ParallelTiledVAE):
self.temperal_upsample = temperal_downsample[::-1]
self.input_channels = config.arch_config.input_channels
self.latents_mean = config.arch_config.latents_mean
self.vae_config = config
self.config = config.arch_config
self.use_parallel_decode = config.use_parallel_decode
self._spatial_parallel_decode_enabled = (
can_install_spatial_shard_parallel_decode(config)
)
self.encoder = QwenImageEncoder3d(
base_dim, z_dim * 2, dim_mult, num_res_blocks, attn_scales, self.temperal_downsample, dropout,
@@ -803,7 +925,8 @@ class AutoencoderKLQwenImage(ParallelTiledVAE):
self.decoder = QwenImageDecoder3d(
base_dim, z_dim, dim_mult, num_res_blocks, attn_scales, self.temperal_upsample, dropout,
input_channels=self.input_channels
input_channels=self.input_channels,
use_parallel_decode=self._spatial_parallel_decode_enabled,
)
# When decoding a batch of video latents at a time, one can save memory by slicing across the batch dimension
@@ -825,7 +948,10 @@ class AutoencoderKLQwenImage(ParallelTiledVAE):
# Precompute and cache conv counts for encoder and decoder for clear_cache speedup
self._cached_conv_counts = {
"decoder": sum(isinstance(m, QwenImageCausalConv3d) for m in self.decoder.modules())
"decoder": sum(
isinstance(m, (QwenImageCausalConv3d, SpatialParallelCausalConv3d))
for m in self.decoder.modules()
)
if self.decoder is not None
else 0,
"encoder": sum(isinstance(m, QwenImageCausalConv3d) for m in self.encoder.modules())
@@ -902,7 +1028,7 @@ class AutoencoderKLQwenImage(ParallelTiledVAE):
def _count_conv3d(model):
count = 0
for m in model.modules():
if isinstance(m, QwenImageCausalConv3d):
if isinstance(m, (QwenImageCausalConv3d, SpatialParallelCausalConv3d)):
count += 1
return count
@@ -962,30 +1088,29 @@ class AutoencoderKLQwenImage(ParallelTiledVAE):
return posterior
def _should_use_spatial_parallel_decode(self, z: torch.Tensor) -> bool:
return (
self._spatial_parallel_decode_enabled
and should_run_spatial_shard_parallel_decode(self.vae_config, z)
)
def _decode_with_parallel_dispatch(self, z: torch.Tensor) -> DecoderOutput:
if self.use_parallel_decode and get_sp_world_size() > 1:
if self.use_parallel_decode and has_decode_parallel_world():
num_frame = z.shape[2]
num_sample_frames = (num_frame - 1) * self.temporal_compression_ratio + 1
tile_latent_min_height = (
self.tile_sample_min_height // self.spatial_compression_ratio
)
tile_latent_min_width = (
self.tile_sample_min_width // self.spatial_compression_ratio
)
mode = self.parallel_decode_mode
if mode == "auto":
if (
z.shape[-2] > tile_latent_min_height
or z.shape[-1] > tile_latent_min_width
):
mode = "tiled"
else:
mode = "patch"
if mode == "patch":
if self._should_use_spatial_parallel_decode(z):
decoded = self._decode(z)[:, :, :num_sample_frames]
elif self._spatial_parallel_decode_enabled:
with disable_spatial_parallel_decode():
decoded = self._decode(z)[:, :, :num_sample_frames]
elif mode == "patch":
decoded = super().parallel_patch_decode(z)[:, :, :num_sample_frames]
else:
elif mode == "tiled":
decoded = super().parallel_tiled_decode(z)[:, :, :num_sample_frames]
else:
decoded = self._decode(z)[:, :, :num_sample_frames]
return DecoderOutput(sample=decoded)
return DecoderOutput(sample=self._decode(z))
@@ -3,6 +3,7 @@
# SPDX-License-Identifier: Apache-2.0
from abc import ABC, abstractmethod
from functools import lru_cache
from math import isqrt, prod
from typing import Optional, cast
@@ -14,15 +15,66 @@ from diffusers.utils.torch_utils import randn_tensor
from torch import nn
from sglang.multimodal_gen.configs.models import VAEConfig
from sglang.multimodal_gen.configs.models.vaes.base import (
should_use_spatial_shard_parallel_decode,
)
from sglang.multimodal_gen.runtime.distributed import (
get_decode_parallel_group_coordinator,
get_decode_parallel_world_size,
get_sp_parallel_rank,
get_sp_world_size,
model_parallel_is_initialized,
)
from sglang.multimodal_gen.runtime.managers.memory_managers.layerwise_offload import (
LayerwiseOffloadableModuleMixin,
)
@lru_cache(maxsize=1)
def _cached_decode_parallel_world_size(
is_dist_initialized: bool, is_model_parallel_initialized: bool, group_id: int
) -> int:
if not is_dist_initialized or not is_model_parallel_initialized:
return 1
return get_decode_parallel_world_size()
def _decode_parallel_world_size() -> int:
is_dist_initialized = dist.is_initialized()
is_model_parallel_initialized = model_parallel_is_initialized()
if not is_dist_initialized or not is_model_parallel_initialized:
return _cached_decode_parallel_world_size(
is_dist_initialized, is_model_parallel_initialized, 0
)
return _cached_decode_parallel_world_size(
is_dist_initialized,
is_model_parallel_initialized,
id(get_decode_parallel_group_coordinator()),
)
def has_decode_parallel_world() -> bool:
return _decode_parallel_world_size() > 1
def can_install_spatial_shard_parallel_decode(config: VAEConfig | None) -> bool:
world_size = _decode_parallel_world_size()
return (
config is not None
and world_size > 1
and should_use_spatial_shard_parallel_decode(config, world_size=world_size)
)
def should_run_spatial_shard_parallel_decode(
config: VAEConfig, z: torch.Tensor
) -> bool:
world_size = _decode_parallel_world_size()
return world_size > 1 and should_use_spatial_shard_parallel_decode(
config, z, world_size
)
class ParallelTiledVAE(ABC, nn.Module, LayerwiseOffloadableModuleMixin):
layerwise_offload_dit_group_enabled = False
layer_names = [
@@ -115,7 +167,15 @@ class ParallelTiledVAE(ABC, nn.Module, LayerwiseOffloadableModuleMixin):
)
num_sample_frames = (num_frames - 1) * self.temporal_compression_ratio + 1
if self.use_tiling and self.use_parallel_tiling and get_sp_world_size() > 1:
if should_run_spatial_shard_parallel_decode(self.config, z):
return self._decode(z)[:, :, :num_sample_frames]
if (
self.parallel_decode_mode == "tiled"
and self.use_tiling
and self.use_parallel_tiling
and get_sp_world_size() > 1
):
return self.parallel_tiled_decode(z)[:, :, :num_sample_frames]
if (
self.use_tiling
@@ -24,8 +24,24 @@ import torch.nn.functional as F
from sglang.jit_kernel.diffusion.group_norm_silu import apply_group_norm_silu
from sglang.multimodal_gen.configs.models.vaes import HunyuanVAEConfig
from sglang.multimodal_gen.runtime.distributed.parallel_state import (
get_decode_parallel_rank,
get_decode_parallel_world_size,
)
from sglang.multimodal_gen.runtime.layers.activation import get_act_fn
from sglang.multimodal_gen.runtime.models.vaes.common import ParallelTiledVAE
from sglang.multimodal_gen.runtime.layers.parallel_conv import (
SpatialParallelConv3d,
chunk_height_by_sizes,
disable_spatial_parallel_decode,
gather_and_trim_height,
gather_variable_height,
split_height_for_parallel_decode,
)
from sglang.multimodal_gen.runtime.models.vaes.common import (
ParallelTiledVAE,
can_install_spatial_shard_parallel_decode,
should_run_spatial_shard_parallel_decode,
)
def prepare_causal_attention_mask(
@@ -45,6 +61,44 @@ def prepare_causal_attention_mask(
return mask
def _make_spatial_parallel_conv3d(
conv: nn.Conv3d,
*,
height_pad: int,
width_pad: int,
padding_mode: str,
) -> SpatialParallelConv3d:
spatial_conv = SpatialParallelConv3d(
in_channels=conv.in_channels,
out_channels=conv.out_channels,
kernel_size=conv.kernel_size,
stride=conv.stride,
padding=(0, height_pad, width_pad),
dilation=conv.dilation,
groups=conv.groups,
bias=conv.bias is not None,
padding_mode=padding_mode,
)
spatial_conv.weight = conv.weight
spatial_conv.bias = conv.bias
return spatial_conv
def _apply_group_norm_silu(
hidden_states: torch.Tensor,
norm: nn.GroupNorm,
nonlinearity: nn.Module,
spatial_parallel: bool,
) -> torch.Tensor:
if not spatial_parallel:
return apply_group_norm_silu(hidden_states, norm, nonlinearity)
hidden_states, heights = gather_variable_height(hidden_states)
hidden_states = apply_group_norm_silu(
hidden_states.contiguous(), norm, nonlinearity
)
return chunk_height_by_sizes(hidden_states, heights)
class HunyuanVAEAttention(nn.Module):
def __init__(
@@ -142,15 +196,43 @@ class HunyuanVideoCausalConv3d(nn.Module):
kernel_size[2] - 1,
0,
)
self.spatial_parallel_time_padding = (
0,
0,
0,
0,
kernel_size[2] - 1,
0,
)
self.spatial_parallel = False
self.conv = nn.Conv3d(
in_channels, out_channels, kernel_size, stride, padding, dilation, bias=bias
)
def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
hidden_states = F.pad(
hidden_states, self.time_causal_padding, mode=self.pad_mode
def enable_spatial_parallel(self) -> None:
if isinstance(self.conv, SpatialParallelConv3d):
self.spatial_parallel = True
return
if self.conv.kernel_size == (1, 1, 1):
self.spatial_parallel = True
return
self.conv = _make_spatial_parallel_conv3d(
self.conv,
height_pad=self.time_causal_padding[2],
width_pad=self.time_causal_padding[0],
padding_mode=self.pad_mode,
)
self.spatial_parallel = True
def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
padding = (
self.spatial_parallel_time_padding
if self.spatial_parallel
else self.time_causal_padding
)
if any(padding):
hidden_states = F.pad(hidden_states, padding, mode=self.pad_mode)
return self.conv(hidden_states)
@@ -254,18 +336,19 @@ class HunyuanVideoResnetBlockCausal3D(nn.Module):
self.conv_shortcut = HunyuanVideoCausalConv3d(
in_channels, out_channels, 1, 1, 0
)
self.spatial_parallel = False
def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
hidden_states = hidden_states.contiguous()
residual = hidden_states
hidden_states = apply_group_norm_silu(
hidden_states, self.norm1, self.nonlinearity
hidden_states = _apply_group_norm_silu(
hidden_states, self.norm1, self.nonlinearity, self.spatial_parallel
)
hidden_states = self.conv1(hidden_states)
hidden_states = apply_group_norm_silu(
hidden_states, self.norm2, self.nonlinearity
hidden_states = _apply_group_norm_silu(
hidden_states, self.norm2, self.nonlinearity, self.spatial_parallel
)
hidden_states = self.dropout(hidden_states)
hidden_states = self.conv2(hidden_states)
@@ -338,8 +421,33 @@ class HunyuanVideoMidBlock3D(nn.Module):
self.attentions = nn.ModuleList(attentions)
self.resnets = nn.ModuleList(resnets)
self.spatial_parallel = False
self.gradient_checkpointing = False
def _run_attention(
self, attn: HunyuanVAEAttention, hidden_states: torch.Tensor
) -> torch.Tensor:
heights = None
if self.spatial_parallel:
hidden_states, heights = gather_variable_height(hidden_states)
batch_size, num_channels, num_frames, height, width = hidden_states.shape
hidden_states = hidden_states.permute(0, 2, 3, 4, 1).flatten(1, 3)
attention_mask = prepare_causal_attention_mask(
num_frames,
height * width,
hidden_states.dtype,
hidden_states.device,
batch_size=batch_size,
)
hidden_states = attn(hidden_states, attention_mask=attention_mask)
hidden_states = hidden_states.unflatten(1, (num_frames, height, width)).permute(
0, 4, 1, 2, 3
)
if heights is not None:
hidden_states = chunk_height_by_sizes(hidden_states, heights)
return hidden_states
def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
if torch.is_grad_enabled() and self.gradient_checkpointing:
hidden_states = self._gradient_checkpointing_func(
@@ -348,21 +456,7 @@ class HunyuanVideoMidBlock3D(nn.Module):
for attn, resnet in zip(self.attentions, self.resnets[1:], strict=True):
if attn is not None:
batch_size, num_channels, num_frames, height, width = (
hidden_states.shape
)
hidden_states = hidden_states.permute(0, 2, 3, 4, 1).flatten(1, 3)
attention_mask = prepare_causal_attention_mask(
num_frames,
height * width,
hidden_states.dtype,
hidden_states.device,
batch_size=batch_size,
)
hidden_states = attn(hidden_states, attention_mask=attention_mask)
hidden_states = hidden_states.unflatten(
1, (num_frames, height, width)
).permute(0, 4, 1, 2, 3)
hidden_states = self._run_attention(attn, hidden_states)
hidden_states = self._gradient_checkpointing_func(resnet, hidden_states)
@@ -371,21 +465,7 @@ class HunyuanVideoMidBlock3D(nn.Module):
for attn, resnet in zip(self.attentions, self.resnets[1:], strict=True):
if attn is not None:
batch_size, num_channels, num_frames, height, width = (
hidden_states.shape
)
hidden_states = hidden_states.permute(0, 2, 3, 4, 1).flatten(1, 3)
attention_mask = prepare_causal_attention_mask(
num_frames,
height * width,
hidden_states.dtype,
hidden_states.device,
batch_size=batch_size,
)
hidden_states = attn(hidden_states, attention_mask=attention_mask)
hidden_states = hidden_states.unflatten(
1, (num_frames, height, width)
).permute(0, 4, 1, 2, 3)
hidden_states = self._run_attention(attn, hidden_states)
hidden_states = resnet(hidden_states)
@@ -736,6 +816,7 @@ class HunyuanVideoDecoder3D(nn.Module):
block_out_channels[0], out_channels, kernel_size=3
)
self.spatial_parallel = False
self.gradient_checkpointing = False
def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
@@ -757,14 +838,29 @@ class HunyuanVideoDecoder3D(nn.Module):
hidden_states = up_block(hidden_states)
# post-process
hidden_states = apply_group_norm_silu(
hidden_states, self.conv_norm_out, self.conv_act
hidden_states = _apply_group_norm_silu(
hidden_states,
self.conv_norm_out,
self.conv_act,
self.spatial_parallel,
)
hidden_states = self.conv_out(hidden_states)
return hidden_states
def _enable_hunyuan_decoder_spatial_parallel(decoder: nn.Module) -> None:
for module in decoder.modules():
if isinstance(module, HunyuanVideoCausalConv3d):
module.enable_spatial_parallel()
elif isinstance(module, HunyuanVideoResnetBlockCausal3D):
module.spatial_parallel = True
elif isinstance(module, HunyuanVideoMidBlock3D):
module.spatial_parallel = True
elif isinstance(module, HunyuanVideoDecoder3D):
module.spatial_parallel = True
class AutoencoderKLHunyuanVideo(ParallelTiledVAE):
r"""
A VAE model with KL loss for encoding videos into latents and decoding latent representations into videos.
@@ -821,6 +917,16 @@ class AutoencoderKLHunyuanVideo(ParallelTiledVAE):
self.post_quant_conv = nn.Conv3d(
config.latent_channels, config.latent_channels, kernel_size=1
)
self._spatial_parallel_decode_enabled = False
if can_install_spatial_shard_parallel_decode(self.config):
_enable_hunyuan_decoder_spatial_parallel(self.decoder)
self._spatial_parallel_decode_enabled = True
def _should_use_spatial_parallel_decode(self, z: torch.Tensor) -> bool:
return (
self._spatial_parallel_decode_enabled
and should_run_spatial_shard_parallel_decode(self.config, z)
)
def _encode(self, x: torch.Tensor) -> torch.Tensor:
x = self.encoder(x)
@@ -829,7 +935,22 @@ class AutoencoderKLHunyuanVideo(ParallelTiledVAE):
def _decode(self, z: torch.Tensor) -> torch.Tensor:
z = self.post_quant_conv(z)
dec = self.decoder(z)
if self._should_use_spatial_parallel_decode(z):
expected_height = (
z.shape[-2] * self.config.arch_config.spatial_compression_ratio
)
z, expected_height = split_height_for_parallel_decode(
z,
expected_height=expected_height,
world_size=get_decode_parallel_world_size(),
rank=get_decode_parallel_rank(),
)
dec = gather_and_trim_height(self.decoder(z), expected_height)
elif self._spatial_parallel_decode_enabled:
with disable_spatial_parallel_decode():
dec = self.decoder(z)
else:
dec = self.decoder(z)
return dec
def forward(
@@ -12,7 +12,21 @@ from diffusers.models.embeddings import PixArtAlphaCombinedTimestepSizeEmbedding
from diffusers.models.modeling_outputs import AutoencoderKLOutput
from sglang.multimodal_gen.configs.models.vaes.ltx_video import LTXVideoVAEConfig
from sglang.multimodal_gen.runtime.models.vaes.common import ParallelTiledVAE
from sglang.multimodal_gen.runtime.distributed.parallel_state import (
get_decode_parallel_rank,
get_decode_parallel_world_size,
)
from sglang.multimodal_gen.runtime.layers.parallel_conv import (
SpatialParallelConv3d,
disable_spatial_parallel_decode,
gather_and_trim_height,
split_height_for_parallel_decode,
)
from sglang.multimodal_gen.runtime.models.vaes.common import (
ParallelTiledVAE,
can_install_spatial_shard_parallel_decode,
should_run_spatial_shard_parallel_decode,
)
@lru_cache(maxsize=128)
@@ -218,6 +232,29 @@ class LTX2VideoCausalConv3d(nn.Module):
return hidden_states
def _make_spatial_parallel_conv3d(conv: nn.Conv3d) -> SpatialParallelConv3d:
spatial_conv = SpatialParallelConv3d(
in_channels=conv.in_channels,
out_channels=conv.out_channels,
kernel_size=conv.kernel_size,
stride=conv.stride,
padding=conv.padding,
dilation=conv.dilation,
groups=conv.groups,
bias=conv.bias is not None,
padding_mode=conv.padding_mode,
)
spatial_conv.weight = conv.weight
spatial_conv.bias = conv.bias
return spatial_conv
def _enable_ltx_decoder_spatial_parallel(decoder: nn.Module) -> None:
for module in decoder.modules():
if isinstance(module, LTX2VideoCausalConv3d) and type(module.conv) is nn.Conv3d:
module.conv = _make_spatial_parallel_conv3d(module.conv)
# Like LTXVideoResnetBlock3d, but uses new causal Conv3d, normal Conv3d for the conv_shortcut, and the spatial padding
# mode is configurable
class LTX2VideoResnetBlock3d(nn.Module):
@@ -1683,6 +1720,10 @@ class AutoencoderKLLTX2Video(ParallelTiledVAE):
latents_std = torch.ones((latent_channels,), requires_grad=False)
self.register_buffer("latents_mean", latents_mean, persistent=True)
self.register_buffer("latents_std", latents_std, persistent=True)
self._spatial_parallel_decode_enabled = False
if can_install_spatial_shard_parallel_decode(self.config):
_enable_ltx_decoder_spatial_parallel(self.decoder)
self._spatial_parallel_decode_enabled = True
# When decoding a batch of video latents at a time, one can save memory by slicing across the batch dimension
# to perform decoding of a single video latent at a time.
@@ -1714,6 +1755,12 @@ class AutoencoderKLLTX2Video(ParallelTiledVAE):
self.tile_sample_stride_width = 448
self.tile_sample_stride_num_frames = 8
def _should_use_spatial_parallel_decode(self, z: torch.Tensor) -> bool:
return (
self._spatial_parallel_decode_enabled
and should_run_spatial_shard_parallel_decode(self.config, z)
)
def enable_tiling(
self,
tile_sample_min_height: Optional[int] = None,
@@ -1829,7 +1876,24 @@ class AutoencoderKLLTX2Video(ParallelTiledVAE):
):
return self.tiled_decode(z, temb, causal=causal, return_dict=return_dict)
dec = self.decoder(z, temb, causal=causal)
if self._should_use_spatial_parallel_decode(z):
expected_height = (
z.shape[-2] * self.config.arch_config.spatial_compression_ratio
)
z, expected_height = split_height_for_parallel_decode(
z,
expected_height=expected_height,
world_size=get_decode_parallel_world_size(),
rank=get_decode_parallel_rank(),
)
dec = gather_and_trim_height(
self.decoder(z, temb, causal=causal), expected_height
)
elif self._spatial_parallel_decode_enabled:
with disable_spatial_parallel_decode():
dec = self.decoder(z, temb, causal=causal)
else:
dec = self.decoder(z, temb, causal=causal)
if not return_dict:
return (dec,)
@@ -0,0 +1,109 @@
import torch
import torch.nn as nn
from sglang.multimodal_gen.runtime.distributed.parallel_state import (
get_decode_parallel_rank,
get_decode_parallel_world_size,
)
from sglang.multimodal_gen.runtime.layers.parallel_conv import (
SpatialParallelConv2d,
chunk_height_by_sizes,
gather_and_trim_height,
gather_variable_height,
split_for_parallel_decode,
)
def count_decoder_spatial_upsamples(decoder: nn.Module) -> int:
return sum(
len(upsamplers)
for block in getattr(decoder, "up_blocks", [])
if (upsamplers := getattr(block, "upsamplers", None)) is not None
)
def enable_diffusers_decoder_spatial_parallel(decoder: nn.Module) -> int:
_replace_conv2d_modules(decoder)
_patch_groupnorm_modules(decoder)
_patch_attention_modules(decoder)
return count_decoder_spatial_upsamples(decoder)
def spatial_parallel_diffusers_decode(
decoder: nn.Module, z: torch.Tensor, upsample_count: int
) -> torch.Tensor:
z, expected_height = split_for_parallel_decode(
z,
upsample_count=upsample_count,
world_size=get_decode_parallel_world_size(),
rank=get_decode_parallel_rank(),
)
return gather_and_trim_height(decoder(z), expected_height)
def _replace_conv2d_modules(module: nn.Module) -> None:
for name, child in list(module.named_children()):
if type(child) is nn.Conv2d:
setattr(module, name, _make_spatial_conv2d(child))
else:
_replace_conv2d_modules(child)
def _make_spatial_conv2d(conv: nn.Conv2d) -> SpatialParallelConv2d:
spatial_conv = SpatialParallelConv2d(
in_channels=conv.in_channels,
out_channels=conv.out_channels,
kernel_size=conv.kernel_size,
stride=conv.stride,
padding=conv.padding,
dilation=conv.dilation,
groups=conv.groups,
bias=conv.bias is not None,
padding_mode=conv.padding_mode,
)
spatial_conv.weight = conv.weight
spatial_conv.bias = conv.bias
return spatial_conv
def _patch_attention_modules(module: nn.Module) -> None:
for child in module.children():
if child.__class__.__name__ == "Attention":
_patch_attention_forward(child)
_patch_attention_modules(child)
def _patch_groupnorm_modules(module: nn.Module) -> None:
for child in module.children():
if type(child) is nn.GroupNorm:
_patch_groupnorm_forward(child)
else:
_patch_groupnorm_modules(child)
def _patch_groupnorm_forward(norm: nn.GroupNorm) -> None:
original_forward = norm.forward
def spatial_parallel_forward(hidden_states):
if hidden_states.dim() < 4:
return original_forward(hidden_states)
hidden_states, heights = gather_variable_height(hidden_states)
hidden_states = hidden_states.contiguous()
hidden_states = original_forward(hidden_states)
return chunk_height_by_sizes(hidden_states, heights)
norm.forward = spatial_parallel_forward
def _patch_attention_forward(attn: nn.Module) -> None:
original_forward = attn.forward
def spatial_parallel_forward(hidden_states, *args, **kwargs):
if hidden_states.dim() != 4:
return original_forward(hidden_states, *args, **kwargs)
hidden_states, heights = gather_variable_height(hidden_states)
hidden_states = hidden_states.contiguous()
hidden_states = original_forward(hidden_states, *args, **kwargs)
return chunk_height_by_sizes(hidden_states, heights)
attn.forward = spatial_parallel_forward
@@ -1,480 +0,0 @@
from __future__ import annotations
import torch
import torch.nn as nn
import torch.nn.functional as F
from sglang.multimodal_gen.runtime.platforms import current_platform
def _channels_last_3d_supported_by_platform() -> bool:
return hasattr(torch, "channels_last_3d") and (
current_platform.is_cuda() or current_platform.is_rocm()
)
def _conv3d_weight_is_channels_last_3d(weight: torch.Tensor) -> bool:
return (
weight.dim() == 5
and _channels_last_3d_supported_by_platform()
and weight.is_contiguous(memory_format=torch.channels_last_3d)
)
def match_conv3d_input_format(x: torch.Tensor, weight: torch.Tensor) -> torch.Tensor:
if x.dim() == 5 and _conv3d_weight_is_channels_last_3d(weight):
return x.contiguous(memory_format=torch.channels_last_3d)
return x
class AvgDown3D(nn.Module):
def __init__(
self,
in_channels,
out_channels,
factor_t,
factor_s=1,
):
super().__init__()
self.in_channels = in_channels
self.out_channels = out_channels
self.factor_t = factor_t
self.factor_s = factor_s
self.factor = self.factor_t * self.factor_s * self.factor_s
assert in_channels * self.factor % out_channels == 0
self.group_size = in_channels * self.factor // out_channels
def forward(self, x: torch.Tensor) -> torch.Tensor:
pad_t = (self.factor_t - x.shape[2] % self.factor_t) % self.factor_t
pad = (0, 0, 0, 0, pad_t, 0)
x = F.pad(x, pad)
B, C, T, H, W = x.shape
x = x.view(
B,
C,
T // self.factor_t,
self.factor_t,
H // self.factor_s,
self.factor_s,
W // self.factor_s,
self.factor_s,
)
x = x.permute(0, 1, 3, 5, 7, 2, 4, 6).contiguous()
x = x.view(
B,
C * self.factor,
T // self.factor_t,
H // self.factor_s,
W // self.factor_s,
)
x = x.view(
B,
self.out_channels,
self.group_size,
T // self.factor_t,
H // self.factor_s,
W // self.factor_s,
)
x = x.mean(dim=2)
return x
class DupUp3D(nn.Module):
def __init__(
self,
in_channels: int,
out_channels: int,
factor_t,
factor_s=1,
):
super().__init__()
self.in_channels = in_channels
self.out_channels = out_channels
self.factor_t = factor_t
self.factor_s = factor_s
self.factor = self.factor_t * self.factor_s * self.factor_s
assert out_channels * self.factor % in_channels == 0
self.repeats = out_channels * self.factor // in_channels
def forward(self, x: torch.Tensor) -> torch.Tensor:
x = x.repeat_interleave(self.repeats, dim=1)
x = x.view(
x.size(0),
self.out_channels,
self.factor_t,
self.factor_s,
self.factor_s,
x.size(2),
x.size(3),
x.size(4),
)
x = x.permute(0, 1, 5, 2, 6, 3, 7, 4).contiguous()
x = x.view(
x.size(0),
self.out_channels,
x.size(2) * self.factor_t,
x.size(4) * self.factor_s,
x.size(6) * self.factor_s,
)
_first_chunk = first_chunk.get() if first_chunk is not None else None
if _first_chunk:
x = x[:, :, self.factor_t - 1 :, :, :]
return x
class WanCausalConv3d(nn.Conv3d):
r"""
A custom 3D causal convolution layer with feature caching support.
This layer extends the standard Conv3D layer by ensuring causality in the time dimension and handling feature
caching for efficient inference.
"""
def __init__(
self,
in_channels: int,
out_channels: int,
kernel_size: int | tuple[int, int, int],
stride: int | tuple[int, int, int] = 1,
padding: int | tuple[int, int, int] = 0,
) -> None:
super().__init__(
in_channels=in_channels,
out_channels=out_channels,
kernel_size=kernel_size,
stride=stride,
padding=padding,
)
self.padding: tuple[int, int, int]
# Set up causal padding
self._padding: tuple[int, ...] = (
self.padding[2],
self.padding[2],
self.padding[1],
self.padding[1],
2 * self.padding[0],
0,
)
self.padding = (0, 0, 0)
def forward(self, x, cache_x=None):
padding = list(self._padding)
if cache_x is not None and self._padding[4] > 0:
cache_x = cache_x.to(x.device)
x = torch.cat([cache_x, x], dim=2)
padding[4] -= cache_x.shape[2]
x = F.pad(x, padding)
x = (
x if current_platform.is_amp_supported() else x.to(self.weight.dtype)
) # casting needed if amp isn't supported
x = match_conv3d_input_format(x, self.weight)
return super().forward(x)
class WanRMS_norm(nn.Module):
r"""
A custom RMS normalization layer.
"""
def __init__(
self,
dim: int,
channel_first: bool = True,
images: bool = True,
bias: bool = False,
) -> None:
super().__init__()
broadcastable_dims = (1, 1, 1) if not images else (1, 1)
shape = (dim, *broadcastable_dims) if channel_first else (dim,)
self.channel_first = channel_first
self.scale = dim**0.5
self.gamma = nn.Parameter(torch.ones(shape))
self.bias = nn.Parameter(torch.zeros(shape)) if bias else 0.0
def forward(self, x):
return (
F.normalize(x, dim=(1 if self.channel_first else -1))
* self.scale
* self.gamma
+ self.bias
)
class WanUpsample(nn.Upsample):
r"""
Perform upsampling while ensuring the output tensor has the same data type as the input.
"""
def forward(self, x):
if current_platform.is_amp_supported():
return super().forward(x)
return super().forward(x.float()).type_as(x)
is_first_frame = None
feat_cache = None
feat_idx = None
cache_t = None
first_chunk = None
def bind_context(
is_first_frame_var,
feat_cache_var,
feat_idx_var,
cache_t_value,
first_chunk_var,
):
global is_first_frame
global feat_cache
global feat_idx
global cache_t
global first_chunk
is_first_frame = is_first_frame_var
feat_cache = feat_cache_var
feat_idx = feat_idx_var
cache_t = cache_t_value
first_chunk = first_chunk_var
def _ensure_bound():
if (
is_first_frame is None
or feat_cache is None
or feat_idx is None
or cache_t is None
or first_chunk is None
):
raise RuntimeError("common_utils.bind_context() must be called before use.")
def resample_forward(self, x):
_ensure_bound()
b, c, t, h, w = x.size()
first_frame = is_first_frame.get()
if first_frame:
assert t == 1
_feat_cache = feat_cache.get()
_feat_idx = feat_idx.get()
if self.mode == "upsample3d":
if _feat_cache is not None:
idx = _feat_idx
if _feat_cache[idx] is None:
_feat_cache[idx] = "Rep"
_feat_idx += 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 last frame of last two chunk
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 += 1
x = x.reshape(b, 2, c, t, h, w)
x = torch.stack((x[:, 0, :, :, :, :], x[:, 1, :, :, :, :]), 3)
x = x.reshape(b, c, t * 2, h, w)
feat_cache.set(_feat_cache)
feat_idx.set(_feat_idx)
elif not first_frame and hasattr(self, "time_conv"):
x = self.time_conv(x)
x = x.reshape(b, 2, c, t, h, w)
x = torch.stack((x[:, 0, :, :, :, :], x[:, 1, :, :, :, :]), 3)
x = x.reshape(b, c, t * 2, h, w)
t = x.shape[2]
x = x.permute(0, 2, 1, 3, 4).reshape(b * t, c, h, w)
x = self.resample(x)
x = x.view(b, t, x.size(1), x.size(2), x.size(3)).permute(0, 2, 1, 3, 4)
_feat_cache = feat_cache.get()
_feat_idx = feat_idx.get()
if self.mode == "downsample3d":
if _feat_cache is not None:
idx = _feat_idx
if _feat_cache[idx] is None:
_feat_cache[idx] = x.clone()
_feat_idx += 1
else:
cache_x = x[:, :, -1:, :, :].clone()
x = self.time_conv(torch.cat([_feat_cache[idx][:, :, -1:, :, :], x], 2))
_feat_cache[idx] = cache_x
_feat_idx += 1
feat_cache.set(_feat_cache)
feat_idx.set(_feat_idx)
elif not first_frame and hasattr(self, "time_conv"):
x = self.time_conv(x)
return x
def residual_block_forward(self, x):
_ensure_bound()
# Apply shortcut connection
h = self.conv_shortcut(x)
# First normalization and activation
x = self.norm1(x)
x = self.nonlinearity(x)
_feat_cache = feat_cache.get()
_feat_idx = feat_idx.get()
if _feat_cache is not None:
idx = _feat_idx
cache_x = x[:, :, -cache_t:, :, :].clone()
if cache_x.shape[2] < 2 and _feat_cache[idx] is not None:
cache_x = torch.cat(
[
_feat_cache[idx][:, :, -1, :, :].unsqueeze(2).to(cache_x.device),
cache_x,
],
dim=2,
)
x = self.conv1(x, _feat_cache[idx])
_feat_cache[idx] = cache_x
_feat_idx += 1
feat_cache.set(_feat_cache)
feat_idx.set(_feat_idx)
else:
x = self.conv1(x)
# Second normalization and activation
x = self.norm2(x)
x = self.nonlinearity(x)
# Dropout
x = self.dropout(x)
_feat_cache = feat_cache.get()
_feat_idx = feat_idx.get()
if _feat_cache is not None:
idx = _feat_idx
cache_x = x[:, :, -cache_t:, :, :].clone()
if cache_x.shape[2] < 2 and _feat_cache[idx] is not None:
cache_x = torch.cat(
[
_feat_cache[idx][:, :, -1, :, :].unsqueeze(2).to(cache_x.device),
cache_x,
],
dim=2,
)
x = self.conv2(x, _feat_cache[idx])
_feat_cache[idx] = cache_x
_feat_idx += 1
feat_cache.set(_feat_cache)
feat_idx.set(_feat_idx)
else:
x = self.conv2(x)
# Add residual connection
return x + h
def attention_block_forward(self, x):
identity = x
batch_size, channels, num_frames, height, width = x.size()
x = x.permute(0, 2, 1, 3, 4).reshape(
batch_size * num_frames, channels, height, width
)
x = self.norm(x)
# compute query, key, value
qkv = self.to_qkv(x)
qkv = qkv.reshape(batch_size * num_frames, 1, channels * 3, -1)
qkv = qkv.permute(0, 1, 3, 2).contiguous()
q, k, v = qkv.chunk(3, dim=-1)
x = torch.nn.functional.scaled_dot_product_attention(q, k, v)
x = (
x.squeeze(1)
.permute(0, 2, 1)
.reshape(batch_size * num_frames, channels, height, width)
)
# output projection
x = self.proj(x)
# Reshape back: [(b*t), c, h, w] -> [b, c, t, h, w]
x = x.view(batch_size, num_frames, channels, height, width)
x = x.permute(0, 2, 1, 3, 4)
return x + identity
def mid_block_forward(self, x):
# First residual block
x = self.resnets[0](x)
# Process through attention and residual blocks
for attn, resnet in zip(self.attentions, self.resnets[1:], strict=True):
if attn is not None:
x = attn(x)
x = resnet(x)
return x
def residual_down_block_forward(self, x):
x_copy = x
for resnet in self.resnets:
x = resnet(x)
if self.downsampler is not None:
x = self.downsampler(x)
return x + self.avg_shortcut(x_copy)
def residual_up_block_forward(self, x):
if self.avg_shortcut is not None:
x_copy = x
for resnet in self.resnets:
x = resnet(x)
if self.upsampler is not None:
x = self.upsampler(x)
if self.avg_shortcut is not None:
x = x + self.avg_shortcut(x_copy)
return x
def up_block_forward(self, x):
for resnet in self.resnets:
x = resnet(x)
if self.upsamplers is not None:
x = self.upsamplers[0](x)
return x
@@ -1,716 +0,0 @@
import math
import torch
import torch.distributed as dist
import torch.nn as nn
import torch.nn.functional as F
from sglang.multimodal_gen.runtime.distributed.parallel_state import (
get_decode_parallel_group_coordinator,
get_decode_parallel_rank,
get_decode_parallel_world_size,
)
from sglang.multimodal_gen.runtime.layers.activation import get_act_fn
from sglang.multimodal_gen.runtime.models.vaes.parallel.wan_common_utils import (
AvgDown3D,
DupUp3D,
WanCausalConv3d,
WanRMS_norm,
WanUpsample,
attention_block_forward,
match_conv3d_input_format,
mid_block_forward,
resample_forward,
residual_block_forward,
residual_down_block_forward,
residual_up_block_forward,
up_block_forward,
)
from sglang.multimodal_gen.runtime.platforms import current_platform
def tensor_pad(x: torch.Tensor, len_to_pad: int, dim: int = -2):
x = torch.cat(
[
x,
torch.zeros(
*x.shape[:dim],
len_to_pad,
*x.shape[dim + 1 :],
dtype=x.dtype,
device=x.device,
),
],
dim=dim,
)
return x
def tensor_chunk(x: torch.Tensor, dim: int = -2, world_size: int = 1, rank: int = 0):
if x is None:
return None
if world_size <= 1:
return x
len_to_padding = (int(math.ceil(x.shape[dim] / world_size)) * world_size) - x.shape[
dim
]
if len_to_padding != 0:
x = tensor_pad(x, len_to_padding, dim=dim)
return torch.chunk(x, world_size, dim=dim)[rank]
def split_for_parallel_encode(
x: torch.Tensor, downsample_count: int, world_size: int, rank: int
):
orig_height = x.shape[-2]
expected_height = orig_height // (2**downsample_count)
factor = world_size * (2**downsample_count)
pad_h = (factor - orig_height % factor) % factor
if pad_h:
x = F.pad(x, (0, 0, 0, pad_h, 0, 0))
expected_local_height = (orig_height + pad_h) // (2**downsample_count) // world_size
x = tensor_chunk(x, dim=-2, world_size=world_size, rank=rank)
return x, expected_height, expected_local_height
def ensure_local_height(x: torch.Tensor, expected_local_height: int | None):
if expected_local_height is None:
return x
if x.shape[-2] < expected_local_height:
pad = expected_local_height - x.shape[-2]
return F.pad(x, (0, 0, 0, pad, 0, 0))
if x.shape[-2] > expected_local_height:
return x[..., :expected_local_height, :].contiguous()
return x
def split_for_parallel_decode(
x: torch.Tensor, upsample_count: int, world_size: int, rank: int
):
expected_height = x.shape[-2] * (2**upsample_count)
x = tensor_chunk(x, dim=-2, world_size=world_size, rank=rank)
return x, expected_height
def _maybe_contiguous_for_sp_gather(x: torch.Tensor) -> torch.Tensor:
if (
x.dim() == 5
and hasattr(torch, "channels_last_3d")
and x.is_contiguous(memory_format=torch.channels_last_3d)
and not x.is_contiguous()
):
return x.contiguous()
return x
def _halo_memory_format(reference: torch.Tensor) -> torch.memory_format:
if reference.dim() > 1 and reference.stride(1) == 1:
if reference.dim() == 5 and hasattr(torch, "channels_last_3d"):
return torch.channels_last_3d
if reference.dim() == 4:
return torch.channels_last
return torch.contiguous_format
def gather_and_trim_height(x: torch.Tensor, expected_height: int | None):
if expected_height is None:
return x
x = get_decode_parallel_group_coordinator().all_gather(
_maybe_contiguous_for_sp_gather(x), dim=-2
)
if x.shape[-2] != expected_height:
x = x[..., :expected_height, :].contiguous()
return x
def _ensure_recv_buf(
recv_buf: torch.Tensor | None, reference: torch.Tensor
) -> torch.Tensor:
memory_format = _halo_memory_format(reference)
if (
recv_buf is None
or recv_buf.shape != reference.shape
or recv_buf.dtype != reference.dtype
or recv_buf.device != reference.device
or not recv_buf.is_contiguous(memory_format=memory_format)
):
return torch.empty(
reference.shape,
dtype=reference.dtype,
device=reference.device,
memory_format=memory_format,
)
return recv_buf
def halo_exchange(
x: torch.Tensor,
height_halo_size: int = 1,
recv_top_buf: torch.Tensor | None = None,
recv_bottom_buf: torch.Tensor | None = None,
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
if height_halo_size == 0:
return x, recv_top_buf, recv_bottom_buf
decode_group = get_decode_parallel_group_coordinator()
rank = get_decode_parallel_rank()
world_size = get_decode_parallel_world_size()
group = decode_group.device_group
group_ranks = decode_group.ranks
top_row_ref = x[..., :height_halo_size, :]
bottom_row_ref = x[..., -height_halo_size:, :]
recv_top_buf = _ensure_recv_buf(recv_top_buf, top_row_ref)
recv_bottom_buf = _ensure_recv_buf(recv_bottom_buf, bottom_row_ref)
# use batched P2P operations
p2p_ops = []
if rank > 0:
# has previous neighbor, recv previous rank's data to recv_top_buf and send top_row to it.
prev_rank = group_ranks[rank - 1]
top_row = top_row_ref.contiguous(memory_format=_halo_memory_format(top_row_ref))
p2p_ops.append(dist.P2POp(dist.irecv, recv_top_buf, prev_rank, group))
p2p_ops.append(dist.P2POp(dist.isend, top_row, prev_rank, group))
if rank < world_size - 1:
# has next neighbor, send bottom_row to next rank and recv next rank's data to recv_bottom_buf.
next_rank = group_ranks[rank + 1]
bottom_row = bottom_row_ref.contiguous(
memory_format=_halo_memory_format(bottom_row_ref)
)
p2p_ops.append(dist.P2POp(dist.isend, bottom_row, next_rank, group))
p2p_ops.append(dist.P2POp(dist.irecv, recv_bottom_buf, next_rank, group))
if rank == 0:
recv_top_buf.zero_()
if rank == world_size - 1:
recv_bottom_buf.zero_()
if p2p_ops:
reqs = dist.batch_isend_irecv(p2p_ops)
for req in reqs:
req.wait()
return (
torch.concat([recv_top_buf, x, recv_bottom_buf], dim=-2),
recv_top_buf,
recv_bottom_buf,
)
class WanDistConv2d(nn.Conv2d):
def __init__(
self,
in_channels: int,
out_channels: int,
kernel_size: int | tuple[int, int, int],
stride: int | tuple[int, int, int] = 1,
padding: int | tuple[int, int, int] = 0,
height_padding: tuple[int, int] | None = None,
):
super().__init__(
in_channels=in_channels,
out_channels=out_channels,
kernel_size=kernel_size,
stride=stride,
padding=padding,
)
self.height_halo_size = (self.kernel_size[-2] - 1) // 2
if height_padding is None:
height_padding = (self.padding[-2], self.padding[-2])
self.height_pad_top, self.height_pad_bottom = height_padding
self.padding: tuple[int, int]
if self.height_halo_size > 0:
self._padding = (0, 0, 0, 0)
else:
self._padding = (
0,
0,
self.padding[0],
self.padding[0],
)
self.padding = (0, self.padding[1])
self._halo_recv_top_buf: torch.Tensor | None = None
self._halo_recv_bottom_buf: torch.Tensor | None = None
self.rank = get_decode_parallel_rank()
self.world_size = get_decode_parallel_world_size()
def forward(self, x):
if any(self._padding):
x = F.pad(x, self._padding)
x_padded, self._halo_recv_top_buf, self._halo_recv_bottom_buf = halo_exchange(
x,
height_halo_size=self.height_halo_size,
recv_top_buf=self._halo_recv_top_buf,
recv_bottom_buf=self._halo_recv_bottom_buf,
)
pad_top = self.height_pad_top
stride = self.stride[-2]
global_start = self.rank * x.shape[-2]
if self.height_halo_size > 0 and stride > 1:
shift = (global_start - self.height_halo_size + pad_top) % stride
if shift:
x_padded = x_padded[..., shift:, :]
global_start += shift
out = super().forward(x_padded)
if self.height_halo_size == 0:
return out
local_height = x.shape[-2]
global_height = local_height * self.world_size
halo = self.height_halo_size
pad_bottom = self.height_pad_bottom
kernel = self.kernel_size[-2]
min_i = math.ceil(((-pad_top) - (global_start - halo)) / stride)
max_i = math.floor(
((global_height - 1 + pad_bottom) - (kernel - 1) - (global_start - halo))
/ stride
)
start = max(min_i, 0)
end = min(max_i + 1, out.shape[-2])
if start != 0 or end != out.shape[-2]:
out = out[..., start:end, :]
return out
class WanDistCausalConv3d(nn.Conv3d):
def __init__(
self,
in_channels: int,
out_channels: int,
kernel_size: int | tuple[int, int, int],
stride: int | tuple[int, int, int] = 1,
padding: int | tuple[int, int, int] = 0,
):
super().__init__(
in_channels=in_channels,
out_channels=out_channels,
kernel_size=kernel_size,
stride=stride,
padding=padding,
)
self.height_pad_top = self.padding[1]
self.height_pad_bottom = self.padding[1]
self.height_halo_size = (self.kernel_size[-2] - 1) // 2
self.padding: tuple[int, int, int]
# Set up causal padding, let the halo to control height padding
if self.height_halo_size > 0:
self._padding: tuple[int, ...] = (
self.padding[2],
self.padding[2],
0,
0,
2 * self.padding[0],
0,
)
else:
self._padding: tuple[int, ...] = (
self.padding[2],
self.padding[2],
self.padding[1],
self.padding[1],
2 * self.padding[0],
0,
)
self.padding = (0, 0, 0)
self._halo_recv_top_buf: torch.Tensor | None = None
self._halo_recv_bottom_buf: torch.Tensor | None = None
self.rank = get_decode_parallel_rank()
self.world_size = get_decode_parallel_world_size()
def forward(self, x, cache_x=None):
padding = list(self._padding)
if cache_x is not None and self._padding[4] > 0:
cache_x = cache_x.to(x.device)
x = torch.cat([cache_x, x], dim=2)
padding[4] -= cache_x.shape[2]
x = F.pad(x, padding)
x = (
x if current_platform.is_amp_supported() else x.to(self.weight.dtype)
) # casting needed if amp isn't supported
x_padded, self._halo_recv_top_buf, self._halo_recv_bottom_buf = halo_exchange(
x,
height_halo_size=self.height_halo_size,
recv_top_buf=self._halo_recv_top_buf,
recv_bottom_buf=self._halo_recv_bottom_buf,
)
pad_top = self.height_pad_top
stride = self.stride[-2]
global_start = self.rank * x.shape[-2]
if self.height_halo_size > 0 and stride > 1:
shift = (global_start - self.height_halo_size + pad_top) % stride
if shift:
x_padded = x_padded[..., shift:, :]
global_start += shift
x_padded = match_conv3d_input_format(x_padded, self.weight)
out = super().forward(x_padded)
if self.height_halo_size == 0:
return out
local_height = x.shape[-2]
global_height = local_height * self.world_size
halo = self.height_halo_size
pad_bottom = self.height_pad_bottom
kernel = self.kernel_size[-2]
min_i = math.ceil(((-pad_top) - (global_start - halo)) / stride)
max_i = math.floor(
((global_height - 1 + pad_bottom) - (kernel - 1) - (global_start - halo))
/ stride
)
start = max(min_i, 0)
end = min(max_i + 1, out.shape[-2])
if start != 0 or end != out.shape[-2]:
out = out[..., start:end, :]
return out
class WanDistZeroPad2d(nn.Module):
"""Apply 2D padding once globally across sequence-parallel height splits."""
def __init__(self, padding: tuple[int, int, int, int]) -> None:
super().__init__()
self.padding = padding # (left, right, top, bottom)
self.rank = get_decode_parallel_rank()
self.world_size = get_decode_parallel_world_size()
def forward(self, x: torch.Tensor) -> torch.Tensor:
left, right, top, bottom = self.padding
if self.world_size <= 1:
return F.pad(x, (left, right, top, bottom))
# Only the first/last rank should contribute global top/bottom padding.
top = top if self.rank == 0 else 0
bottom = bottom if self.rank == self.world_size - 1 else 0
return F.pad(x, (left, right, top, bottom))
class WanDistResample(nn.Module):
r"""
A custom resampling module for 2D and 3D data used for parallel decoding.
Args:
dim (int): The number of input/output channels.
mode (str): The resampling mode. Must be one of:
- 'none': No resampling (identity operation).
- 'upsample2d': 2D upsampling with nearest-exact interpolation and convolution.
- 'upsample3d': 3D upsampling with nearest-exact interpolation, convolution, and causal 3D convolution.
- 'downsample2d': 2D downsampling with zero-padding and convolution.
- 'downsample3d': 3D downsampling with zero-padding, convolution, and causal 3D convolution.
"""
def __init__(self, dim: int, mode: str, upsample_out_dim: int = None) -> None:
super().__init__()
self.dim = dim
self.mode = mode
# default to dim //2
if upsample_out_dim is None:
upsample_out_dim = dim // 2
# layers
# We support parallel encode/decode; downsample uses halo exchange as well.
if mode == "upsample2d":
self.resample = nn.Sequential(
WanUpsample(scale_factor=(2.0, 2.0), mode="nearest-exact"),
WanDistConv2d(dim, upsample_out_dim, 3, padding=1),
)
elif mode == "upsample3d":
self.resample = nn.Sequential(
WanUpsample(scale_factor=(2.0, 2.0), mode="nearest-exact"),
WanDistConv2d(dim, upsample_out_dim, 3, padding=1),
)
self.time_conv = WanCausalConv3d(dim, dim * 2, (3, 1, 1), padding=(1, 0, 0))
elif mode == "downsample2d":
self.resample = nn.Sequential(
WanDistZeroPad2d((0, 1, 0, 0)),
WanDistConv2d(dim, dim, 3, stride=(2, 2), height_padding=(0, 1)),
)
elif mode == "downsample3d":
self.resample = nn.Sequential(
WanDistZeroPad2d((0, 1, 0, 0)),
WanDistConv2d(dim, dim, 3, stride=(2, 2), height_padding=(0, 1)),
)
self.time_conv = WanCausalConv3d(
dim, dim, (3, 1, 1), stride=(2, 1, 1), padding=(0, 0, 0)
)
else:
self.resample = nn.Identity()
def forward(self, x):
return resample_forward(self, x)
class WanDistResidualBlock(nn.Module):
r"""
A custom residual block module.
Args:
in_dim (int): Number of input channels.
out_dim (int): Number of output channels.
dropout (float, optional): Dropout rate for the dropout layer. Default is 0.0.
non_linearity (str, optional): Type of non-linearity to use. Default is "silu".
"""
def __init__(
self,
in_dim: int,
out_dim: int,
dropout: float = 0.0,
non_linearity: str = "silu",
) -> None:
super().__init__()
self.in_dim = in_dim
self.out_dim = out_dim
self.nonlinearity = get_act_fn(non_linearity)
# layers
self.norm1 = WanRMS_norm(in_dim, images=False)
self.conv1 = WanDistCausalConv3d(in_dim, out_dim, 3, padding=1)
self.norm2 = WanRMS_norm(out_dim, images=False)
self.dropout = nn.Dropout(dropout)
self.conv2 = WanDistCausalConv3d(out_dim, out_dim, 3, padding=1)
self.conv_shortcut = (
WanDistCausalConv3d(in_dim, out_dim, 1)
if in_dim != out_dim
else nn.Identity()
)
def forward(self, x):
return residual_block_forward(self, x)
class WanDistAttentionBlock(nn.Module):
r"""
Causal self-attention with a single head.
Args:
dim (int): The number of channels in the input tensor.
"""
def __init__(self, dim) -> None:
super().__init__()
self.dim = dim
# layers
self.norm = WanRMS_norm(dim)
self.to_qkv = nn.Conv2d(dim, dim * 3, 1)
self.proj = nn.Conv2d(dim, dim, 1)
self.rank = get_decode_parallel_rank()
self.world_size = get_decode_parallel_world_size()
self.decode_group = get_decode_parallel_group_coordinator()
def forward(self, x):
if self.world_size > 1:
x = self.decode_group.all_gather(_maybe_contiguous_for_sp_gather(x), dim=-2)
x = x.contiguous()
x = attention_block_forward(self, x)
if self.world_size > 1:
x = torch.chunk(x, self.world_size, dim=-2)[self.rank]
return x
class WanDistMidBlock(nn.Module):
"""
Middle block for WanVAE encoder and decoder.
Args:
dim (int): Number of input/output channels.
dropout (float): Dropout rate.
non_linearity (str): Type of non-linearity to use.
"""
def __init__(
self,
dim: int,
dropout: float = 0.0,
non_linearity: str = "silu",
num_layers: int = 1,
):
super().__init__()
self.dim = dim
# Create the components
resnets = [WanDistResidualBlock(dim, dim, dropout, non_linearity)]
attentions = []
for _ in range(num_layers):
attentions.append(WanDistAttentionBlock(dim))
resnets.append(WanDistResidualBlock(dim, dim, dropout, non_linearity))
self.attentions = nn.ModuleList(attentions)
self.resnets = nn.ModuleList(resnets)
self.gradient_checkpointing = False
def forward(self, x):
return mid_block_forward(self, x)
class WanDistResidualDownBlock(nn.Module):
def __init__(
self,
in_dim,
out_dim,
dropout,
num_res_blocks,
temperal_downsample=False,
down_flag=False,
):
super().__init__()
# Shortcut path with downsample
self.avg_shortcut = AvgDown3D(
in_dim,
out_dim,
factor_t=2 if temperal_downsample else 1,
factor_s=2 if down_flag else 1,
)
# Main path with residual blocks and downsample
resnets = []
for _ in range(num_res_blocks):
resnets.append(WanDistResidualBlock(in_dim, out_dim, dropout))
in_dim = out_dim
self.resnets = nn.ModuleList(resnets)
# Add the final downsample block
if down_flag:
mode = "downsample3d" if temperal_downsample else "downsample2d"
self.downsampler = WanDistResample(out_dim, mode=mode)
else:
self.downsampler = None
def forward(self, x):
return residual_down_block_forward(self, x)
class WanDistResidualUpBlock(nn.Module):
"""
A block that handles upsampling for the WanVAE decoder.
Args:
in_dim (int): Input dimension
out_dim (int): Output dimension
num_res_blocks (int): Number of residual blocks
dropout (float): Dropout rate
temperal_upsample (bool): Whether to upsample on temporal dimension
up_flag (bool): Whether to upsample or not
non_linearity (str): Type of non-linearity to use
"""
def __init__(
self,
in_dim: int,
out_dim: int,
num_res_blocks: int,
dropout: float = 0.0,
temperal_upsample: bool = False,
up_flag: bool = False,
non_linearity: str = "silu",
):
super().__init__()
self.in_dim = in_dim
self.out_dim = out_dim
if up_flag:
self.avg_shortcut = DupUp3D(
in_dim,
out_dim,
factor_t=2 if temperal_upsample else 1,
factor_s=2,
)
else:
self.avg_shortcut = None
# create residual blocks
resnets = []
current_dim = in_dim
for _ in range(num_res_blocks + 1):
resnets.append(
WanDistResidualBlock(current_dim, out_dim, dropout, non_linearity)
)
current_dim = out_dim
self.resnets = nn.ModuleList(resnets)
# Add upsampling layer if needed
if up_flag:
upsample_mode = "upsample3d" if temperal_upsample else "upsample2d"
self.upsampler = WanDistResample(
out_dim, mode=upsample_mode, upsample_out_dim=out_dim
)
else:
self.upsampler = None
self.gradient_checkpointing = False
def forward(self, x):
return residual_up_block_forward(self, x)
class WanDistUpBlock(nn.Module):
"""
A block that handles upsampling for the WanVAE decoder.
Args:
in_dim (int): Input dimension
out_dim (int): Output dimension
num_res_blocks (int): Number of residual blocks
dropout (float): Dropout rate
upsample_mode (str, optional): Mode for upsampling ('upsample2d' or 'upsample3d')
non_linearity (str): Type of non-linearity to use
"""
def __init__(
self,
in_dim: int,
out_dim: int,
num_res_blocks: int,
dropout: float = 0.0,
upsample_mode: str | None = None,
non_linearity: str = "silu",
):
super().__init__()
self.in_dim = in_dim
self.out_dim = out_dim
# Create layers list
resnets = []
# Add residual blocks and attention if needed
current_dim = in_dim
for _ in range(num_res_blocks + 1):
resnets.append(
WanDistResidualBlock(current_dim, out_dim, dropout, non_linearity)
)
current_dim = out_dim
self.resnets = nn.ModuleList(resnets)
# Add upsampling layer if needed
self.upsamplers = None
if upsample_mode is not None:
self.upsamplers = nn.ModuleList(
[WanDistResample(out_dim, mode=upsample_mode)]
)
self.gradient_checkpointing = False
def forward(self, x):
return up_block_forward(self, x)
@@ -17,14 +17,18 @@
# limitations under the License.
import contextvars
from contextlib import contextmanager
from contextlib import contextmanager, nullcontext
import torch
import torch.distributed as dist
import torch.nn as nn
import torch.nn.functional as F
from einops import rearrange
from sglang.multimodal_gen.configs.models.vaes import WanVAEConfig
from sglang.multimodal_gen.configs.models.vaes.base import (
should_use_spatial_shard_parallel_decode,
)
from sglang.multimodal_gen.runtime.distributed.parallel_state import (
get_decode_parallel_rank,
get_decode_parallel_world_size,
@@ -32,39 +36,22 @@ from sglang.multimodal_gen.runtime.distributed.parallel_state import (
get_sp_world_size,
)
from sglang.multimodal_gen.runtime.layers.activation import get_act_fn
from sglang.multimodal_gen.runtime.layers.parallel_conv import (
SpatialParallelCausalConv3d,
SpatialParallelConv2d,
SpatialParallelZeroPad2d,
chunk_height_for_parallel_decode,
disable_spatial_parallel_decode,
gather_and_trim_height,
gather_height_for_global_op,
split_for_parallel_decode,
)
from sglang.multimodal_gen.runtime.models.vaes.common import (
DiagonalGaussianDistribution,
ParallelTiledVAE,
should_run_spatial_shard_parallel_decode,
)
from sglang.multimodal_gen.runtime.models.vaes.parallel.wan_common_utils import (
AvgDown3D,
DupUp3D,
WanCausalConv3d,
WanRMS_norm,
WanUpsample,
attention_block_forward,
bind_context,
mid_block_forward,
resample_forward,
residual_block_forward,
residual_down_block_forward,
residual_up_block_forward,
up_block_forward,
)
from sglang.multimodal_gen.runtime.models.vaes.parallel.wan_dist_utils import (
WanDistAttentionBlock,
WanDistCausalConv3d,
WanDistMidBlock,
WanDistResample,
WanDistResidualBlock,
WanDistResidualDownBlock,
WanDistResidualUpBlock,
WanDistUpBlock,
ensure_local_height,
gather_and_trim_height,
split_for_parallel_decode,
split_for_parallel_encode,
)
from sglang.multimodal_gen.runtime.platforms import current_platform
CACHE_T = 2
@@ -73,7 +60,464 @@ feat_cache = contextvars.ContextVar("feat_cache", default=None)
feat_idx = contextvars.ContextVar("feat_idx", default=0)
first_chunk = contextvars.ContextVar("first_chunk", default=None)
bind_context(is_first_frame, feat_cache, feat_idx, CACHE_T, first_chunk)
def _channels_last_3d_supported_by_platform() -> bool:
return hasattr(torch, "channels_last_3d") and (
current_platform.is_cuda() or current_platform.is_rocm()
)
def _conv3d_weight_is_channels_last_3d(weight: torch.Tensor) -> bool:
return (
weight.dim() == 5
and _channels_last_3d_supported_by_platform()
and weight.is_contiguous(memory_format=torch.channels_last_3d)
)
def match_conv3d_input_format(x: torch.Tensor, weight: torch.Tensor) -> torch.Tensor:
if x.dim() == 5 and _conv3d_weight_is_channels_last_3d(weight):
return x.contiguous(memory_format=torch.channels_last_3d)
return x
class AvgDown3D(nn.Module):
def __init__(
self,
in_channels,
out_channels,
factor_t,
factor_s=1,
):
super().__init__()
self.in_channels = in_channels
self.out_channels = out_channels
self.factor_t = factor_t
self.factor_s = factor_s
self.factor = self.factor_t * self.factor_s * self.factor_s
assert in_channels * self.factor % out_channels == 0
self.group_size = in_channels * self.factor // out_channels
def forward(self, x: torch.Tensor) -> torch.Tensor:
pad_t = (self.factor_t - x.shape[2] % self.factor_t) % self.factor_t
pad = (0, 0, 0, 0, pad_t, 0)
x = F.pad(x, pad)
B, C, T, H, W = x.shape
x = x.view(
B,
C,
T // self.factor_t,
self.factor_t,
H // self.factor_s,
self.factor_s,
W // self.factor_s,
self.factor_s,
)
x = x.permute(0, 1, 3, 5, 7, 2, 4, 6).contiguous()
x = x.view(
B,
C * self.factor,
T // self.factor_t,
H // self.factor_s,
W // self.factor_s,
)
x = x.view(
B,
self.out_channels,
self.group_size,
T // self.factor_t,
H // self.factor_s,
W // self.factor_s,
)
x = x.mean(dim=2)
return x
class DupUp3D(nn.Module):
def __init__(
self,
in_channels: int,
out_channels: int,
factor_t,
factor_s=1,
):
super().__init__()
self.in_channels = in_channels
self.out_channels = out_channels
self.factor_t = factor_t
self.factor_s = factor_s
self.factor = self.factor_t * self.factor_s * self.factor_s
assert out_channels * self.factor % in_channels == 0
self.repeats = out_channels * self.factor // in_channels
def forward(self, x: torch.Tensor) -> torch.Tensor:
x = x.repeat_interleave(self.repeats, dim=1)
x = x.view(
x.size(0),
self.out_channels,
self.factor_t,
self.factor_s,
self.factor_s,
x.size(2),
x.size(3),
x.size(4),
)
x = x.permute(0, 1, 5, 2, 6, 3, 7, 4).contiguous()
x = x.view(
x.size(0),
self.out_channels,
x.size(2) * self.factor_t,
x.size(4) * self.factor_s,
x.size(6) * self.factor_s,
)
_first_chunk = first_chunk.get() if first_chunk is not None else None
if _first_chunk:
x = x[:, :, self.factor_t - 1 :, :, :]
return x
class WanCausalConv3d(nn.Conv3d):
r"""
A custom 3D causal convolution layer with feature caching support.
This layer extends the standard Conv3D layer by ensuring causality in the time dimension and handling feature
caching for efficient inference.
"""
def __init__(
self,
in_channels: int,
out_channels: int,
kernel_size: int | tuple[int, int, int],
stride: int | tuple[int, int, int] = 1,
padding: int | tuple[int, int, int] = 0,
) -> None:
super().__init__(
in_channels=in_channels,
out_channels=out_channels,
kernel_size=kernel_size,
stride=stride,
padding=padding,
)
self.padding: tuple[int, int, int]
# Set up causal padding
self._padding: tuple[int, ...] = (
self.padding[2],
self.padding[2],
self.padding[1],
self.padding[1],
2 * self.padding[0],
0,
)
self.padding = (0, 0, 0)
def forward(self, x, cache_x=None):
padding = list(self._padding)
if cache_x is not None and self._padding[4] > 0:
cache_x = cache_x.to(x.device)
x = torch.cat([cache_x, x], dim=2)
padding[4] -= cache_x.shape[2]
x = F.pad(x, padding)
x = (
x if current_platform.is_amp_supported() else x.to(self.weight.dtype)
) # casting needed if amp isn't supported
x = match_conv3d_input_format(x, self.weight)
return super().forward(x)
class WanRMS_norm(nn.Module):
r"""
A custom RMS normalization layer.
"""
def __init__(
self,
dim: int,
channel_first: bool = True,
images: bool = True,
bias: bool = False,
) -> None:
super().__init__()
broadcastable_dims = (1, 1, 1) if not images else (1, 1)
shape = (dim, *broadcastable_dims) if channel_first else (dim,)
self.channel_first = channel_first
self.scale = dim**0.5
self.gamma = nn.Parameter(torch.ones(shape))
self.bias = nn.Parameter(torch.zeros(shape)) if bias else 0.0
def forward(self, x):
return (
F.normalize(x, dim=(1 if self.channel_first else -1))
* self.scale
* self.gamma
+ self.bias
)
class WanUpsample(nn.Upsample):
r"""
Perform upsampling while ensuring the output tensor has the same data type as the input.
"""
def forward(self, x):
if current_platform.is_amp_supported():
return super().forward(x)
return super().forward(x.float()).type_as(x)
def resample_forward(self, x):
b, c, t, h, w = x.size()
first_frame = is_first_frame.get()
if first_frame:
assert t == 1
_feat_cache = feat_cache.get()
_feat_idx = feat_idx.get()
if self.mode == "upsample3d":
if _feat_cache is not None:
idx = _feat_idx
if _feat_cache[idx] is None:
_feat_cache[idx] = "Rep"
_feat_idx += 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 last frame of last two chunk
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 += 1
x = x.reshape(b, 2, c, t, h, w)
x = torch.stack((x[:, 0, :, :, :, :], x[:, 1, :, :, :, :]), 3)
x = x.reshape(b, c, t * 2, h, w)
feat_cache.set(_feat_cache)
feat_idx.set(_feat_idx)
elif not first_frame and hasattr(self, "time_conv"):
x = self.time_conv(x)
x = x.reshape(b, 2, c, t, h, w)
x = torch.stack((x[:, 0, :, :, :, :], x[:, 1, :, :, :, :]), 3)
x = x.reshape(b, c, t * 2, h, w)
t = x.shape[2]
x = x.permute(0, 2, 1, 3, 4).reshape(b * t, c, h, w)
x = self.resample(x)
x = x.view(b, t, x.size(1), x.size(2), x.size(3)).permute(0, 2, 1, 3, 4)
_feat_cache = feat_cache.get()
_feat_idx = feat_idx.get()
if self.mode == "downsample3d":
if _feat_cache is not None:
idx = _feat_idx
if _feat_cache[idx] is None:
_feat_cache[idx] = x.clone()
_feat_idx += 1
else:
cache_x = x[:, :, -1:, :, :].clone()
x = self.time_conv(torch.cat([_feat_cache[idx][:, :, -1:, :, :], x], 2))
_feat_cache[idx] = cache_x
_feat_idx += 1
feat_cache.set(_feat_cache)
feat_idx.set(_feat_idx)
elif not first_frame and hasattr(self, "time_conv"):
x = self.time_conv(x)
return x
def residual_block_forward(self, x):
# Apply shortcut connection
h = self.conv_shortcut(x)
# First normalization and activation
x = self.norm1(x)
x = self.nonlinearity(x)
_feat_cache = feat_cache.get()
_feat_idx = feat_idx.get()
if _feat_cache is not None:
idx = _feat_idx
cache_x = x[:, :, -CACHE_T:, :, :].clone()
if cache_x.shape[2] < 2 and _feat_cache[idx] is not None:
cache_x = torch.cat(
[
_feat_cache[idx][:, :, -1, :, :].unsqueeze(2).to(cache_x.device),
cache_x,
],
dim=2,
)
x = self.conv1(x, _feat_cache[idx])
_feat_cache[idx] = cache_x
_feat_idx += 1
feat_cache.set(_feat_cache)
feat_idx.set(_feat_idx)
else:
x = self.conv1(x)
# Second normalization and activation
x = self.norm2(x)
x = self.nonlinearity(x)
# Dropout
x = self.dropout(x)
_feat_cache = feat_cache.get()
_feat_idx = feat_idx.get()
if _feat_cache is not None:
idx = _feat_idx
cache_x = x[:, :, -CACHE_T:, :, :].clone()
if cache_x.shape[2] < 2 and _feat_cache[idx] is not None:
cache_x = torch.cat(
[
_feat_cache[idx][:, :, -1, :, :].unsqueeze(2).to(cache_x.device),
cache_x,
],
dim=2,
)
x = self.conv2(x, _feat_cache[idx])
_feat_cache[idx] = cache_x
_feat_idx += 1
feat_cache.set(_feat_cache)
feat_idx.set(_feat_idx)
else:
x = self.conv2(x)
# Add residual connection
return x + h
def attention_block_forward(self, x):
identity = x
batch_size, channels, num_frames, height, width = x.size()
x = x.permute(0, 2, 1, 3, 4).reshape(
batch_size * num_frames, channels, height, width
)
x = self.norm(x)
# compute query, key, value
qkv = self.to_qkv(x)
qkv = qkv.reshape(batch_size * num_frames, 1, channels * 3, -1)
qkv = qkv.permute(0, 1, 3, 2).contiguous()
q, k, v = qkv.chunk(3, dim=-1)
x = torch.nn.functional.scaled_dot_product_attention(q, k, v)
x = (
x.squeeze(1)
.permute(0, 2, 1)
.reshape(batch_size * num_frames, channels, height, width)
)
# output projection
x = self.proj(x)
# Reshape back: [(b*t), c, h, w] -> [b, c, t, h, w]
x = x.view(batch_size, num_frames, channels, height, width)
x = x.permute(0, 2, 1, 3, 4)
return x + identity
def mid_block_forward(self, x):
# First residual block
x = self.resnets[0](x)
# Process through attention and residual blocks
for attn, resnet in zip(self.attentions, self.resnets[1:], strict=True):
if attn is not None:
x = attn(x)
x = resnet(x)
return x
def residual_down_block_forward(self, x):
x_copy = x
for resnet in self.resnets:
x = resnet(x)
if self.downsampler is not None:
x = self.downsampler(x)
return x + self.avg_shortcut(x_copy)
def residual_up_block_forward(self, x):
if self.avg_shortcut is not None:
x_copy = x
for resnet in self.resnets:
x = resnet(x)
if self.upsampler is not None:
x = self.upsampler(x)
if self.avg_shortcut is not None:
x = x + self.avg_shortcut(x_copy)
return x
def up_block_forward(self, x):
for resnet in self.resnets:
x = resnet(x)
if self.upsamplers is not None:
x = self.upsamplers[0](x)
return x
def split_for_parallel_encode(
x: torch.Tensor, downsample_count: int, world_size: int, rank: int
):
orig_height = x.shape[-2]
expected_height = orig_height // (2**downsample_count)
factor = world_size * (2**downsample_count)
pad_h = (factor - orig_height % factor) % factor
if pad_h:
x = F.pad(x, (0, 0, 0, pad_h, 0, 0))
expected_local_height = (orig_height + pad_h) // (2**downsample_count) // world_size
x = torch.chunk(x, world_size, dim=-2)[rank]
return x, expected_height, expected_local_height
def ensure_local_height(x: torch.Tensor, expected_local_height: int | None):
if expected_local_height is None:
return x
if x.shape[-2] < expected_local_height:
pad = expected_local_height - x.shape[-2]
return F.pad(x, (0, 0, 0, pad, 0, 0))
if x.shape[-2] > expected_local_height:
return x[..., :expected_local_height, :].contiguous()
return x
@contextmanager
@@ -107,7 +551,16 @@ class WanResample(nn.Module):
- 'downsample3d': 3D downsampling with zero-padding, convolution, and causal 3D convolution.
"""
def __init__(self, dim: int, mode: str, upsample_out_dim: int = None) -> None:
def __init__(
self,
dim: int,
mode: str,
upsample_out_dim: int = None,
*,
conv2d_cls=nn.Conv2d,
zero_pad2d_cls=nn.ZeroPad2d,
spatial_parallel: bool = False,
) -> None:
super().__init__()
self.dim = dim
self.mode = mode
@@ -120,23 +573,37 @@ class WanResample(nn.Module):
if mode == "upsample2d":
self.resample = nn.Sequential(
WanUpsample(scale_factor=(2.0, 2.0), mode="nearest-exact"),
nn.Conv2d(dim, upsample_out_dim, 3, padding=1),
conv2d_cls(dim, upsample_out_dim, 3, padding=1),
)
elif mode == "upsample3d":
self.resample = nn.Sequential(
WanUpsample(scale_factor=(2.0, 2.0), mode="nearest-exact"),
nn.Conv2d(dim, upsample_out_dim, 3, padding=1),
conv2d_cls(dim, upsample_out_dim, 3, padding=1),
)
self.time_conv = WanCausalConv3d(dim, dim * 2, (3, 1, 1), padding=(1, 0, 0))
elif mode == "downsample2d":
self.resample = nn.Sequential(
nn.ZeroPad2d((0, 1, 0, 1)), nn.Conv2d(dim, dim, 3, stride=(2, 2))
)
if spatial_parallel:
self.resample = nn.Sequential(
zero_pad2d_cls((0, 1, 0, 0)),
conv2d_cls(dim, dim, 3, stride=(2, 2), height_padding=(0, 1)),
)
else:
self.resample = nn.Sequential(
zero_pad2d_cls((0, 1, 0, 1)),
conv2d_cls(dim, dim, 3, stride=(2, 2)),
)
elif mode == "downsample3d":
self.resample = nn.Sequential(
nn.ZeroPad2d((0, 1, 0, 1)), nn.Conv2d(dim, dim, 3, stride=(2, 2))
)
if spatial_parallel:
self.resample = nn.Sequential(
zero_pad2d_cls((0, 1, 0, 0)),
conv2d_cls(dim, dim, 3, stride=(2, 2), height_padding=(0, 1)),
)
else:
self.resample = nn.Sequential(
zero_pad2d_cls((0, 1, 0, 1)),
conv2d_cls(dim, dim, 3, stride=(2, 2)),
)
self.time_conv = WanCausalConv3d(
dim, dim, (3, 1, 1), stride=(2, 1, 1), padding=(0, 0, 0)
)
@@ -165,6 +632,9 @@ class WanResidualBlock(nn.Module):
out_dim: int,
dropout: float = 0.0,
non_linearity: str = "silu",
*,
causal_conv3d_cls=WanCausalConv3d,
shortcut_conv3d_cls=WanCausalConv3d,
) -> None:
super().__init__()
self.in_dim = in_dim
@@ -173,12 +643,14 @@ class WanResidualBlock(nn.Module):
# layers
self.norm1 = WanRMS_norm(in_dim, images=False)
self.conv1 = WanCausalConv3d(in_dim, out_dim, 3, padding=1)
self.conv1 = causal_conv3d_cls(in_dim, out_dim, 3, padding=1)
self.norm2 = WanRMS_norm(out_dim, images=False)
self.dropout = nn.Dropout(dropout)
self.conv2 = WanCausalConv3d(out_dim, out_dim, 3, padding=1)
self.conv2 = causal_conv3d_cls(out_dim, out_dim, 3, padding=1)
self.conv_shortcut = (
WanCausalConv3d(in_dim, out_dim, 1) if in_dim != out_dim else nn.Identity()
shortcut_conv3d_cls(in_dim, out_dim, 1)
if in_dim != out_dim
else nn.Identity()
)
def forward(self, x):
@@ -193,9 +665,10 @@ class WanAttentionBlock(nn.Module):
dim (int): The number of channels in the input tensor.
"""
def __init__(self, dim) -> None:
def __init__(self, dim, *, spatial_parallel: bool = False) -> None:
super().__init__()
self.dim = dim
self.world_size = get_decode_parallel_world_size() if spatial_parallel else 1
# layers
self.norm = WanRMS_norm(dim)
@@ -203,7 +676,12 @@ class WanAttentionBlock(nn.Module):
self.proj = nn.Conv2d(dim, dim, 1)
def forward(self, x):
return attention_block_forward(self, x)
if self.world_size > 1:
x = gather_height_for_global_op(x).contiguous()
x = attention_block_forward(self, x)
if self.world_size > 1:
x = chunk_height_for_parallel_decode(x)
return x
class WanMidBlock(nn.Module):
@@ -222,16 +700,19 @@ class WanMidBlock(nn.Module):
dropout: float = 0.0,
non_linearity: str = "silu",
num_layers: int = 1,
*,
residual_block_cls=WanResidualBlock,
attention_block_cls=WanAttentionBlock,
):
super().__init__()
self.dim = dim
# Create the components
resnets = [WanResidualBlock(dim, dim, dropout, non_linearity)]
resnets = [residual_block_cls(dim, dim, dropout, non_linearity)]
attentions = []
for _ in range(num_layers):
attentions.append(WanAttentionBlock(dim))
resnets.append(WanResidualBlock(dim, dim, dropout, non_linearity))
attentions.append(attention_block_cls(dim))
resnets.append(residual_block_cls(dim, dim, dropout, non_linearity))
self.attentions = nn.ModuleList(attentions)
self.resnets = nn.ModuleList(resnets)
@@ -251,6 +732,9 @@ class WanResidualDownBlock(nn.Module):
num_res_blocks,
temperal_downsample=False,
down_flag=False,
*,
residual_block_cls=WanResidualBlock,
resample_cls=WanResample,
):
super().__init__()
@@ -265,14 +749,14 @@ class WanResidualDownBlock(nn.Module):
# Main path with residual blocks and downsample
resnets = []
for _ in range(num_res_blocks):
resnets.append(WanResidualBlock(in_dim, out_dim, dropout))
resnets.append(residual_block_cls(in_dim, out_dim, dropout))
in_dim = out_dim
self.resnets = nn.ModuleList(resnets)
# Add the final downsample block
if down_flag:
mode = "downsample3d" if temperal_downsample else "downsample2d"
self.downsampler = WanResample(out_dim, mode=mode)
self.downsampler = resample_cls(out_dim, mode=mode)
else:
self.downsampler = None
@@ -280,6 +764,80 @@ class WanResidualDownBlock(nn.Module):
return residual_down_block_forward(self, x)
class WanDistResample(WanResample):
def __init__(self, dim: int, mode: str, upsample_out_dim: int = None) -> None:
super().__init__(
dim,
mode,
upsample_out_dim=upsample_out_dim,
conv2d_cls=SpatialParallelConv2d,
zero_pad2d_cls=SpatialParallelZeroPad2d,
spatial_parallel=True,
)
class WanDistResidualBlock(WanResidualBlock):
def __init__(
self,
in_dim: int,
out_dim: int,
dropout: float = 0.0,
non_linearity: str = "silu",
) -> None:
super().__init__(
in_dim,
out_dim,
dropout,
non_linearity,
causal_conv3d_cls=SpatialParallelCausalConv3d,
)
class WanDistAttentionBlock(WanAttentionBlock):
def __init__(self, dim) -> None:
super().__init__(dim, spatial_parallel=True)
class WanDistMidBlock(WanMidBlock):
def __init__(
self,
dim: int,
dropout: float = 0.0,
non_linearity: str = "silu",
num_layers: int = 1,
):
super().__init__(
dim,
dropout,
non_linearity,
num_layers=num_layers,
residual_block_cls=WanDistResidualBlock,
attention_block_cls=WanDistAttentionBlock,
)
class WanDistResidualDownBlock(WanResidualDownBlock):
def __init__(
self,
in_dim,
out_dim,
dropout,
num_res_blocks,
temperal_downsample=False,
down_flag=False,
):
super().__init__(
in_dim,
out_dim,
dropout,
num_res_blocks,
temperal_downsample=temperal_downsample,
down_flag=down_flag,
residual_block_cls=WanDistResidualBlock,
resample_cls=WanDistResample,
)
class WanEncoder3d(nn.Module):
r"""
A 3D encoder module.
@@ -330,7 +888,7 @@ class WanEncoder3d(nn.Module):
world_size = get_sp_world_size()
if use_parallel_encode and world_size > 1:
CausalConv3d = WanDistCausalConv3d
CausalConv3d = SpatialParallelCausalConv3d
ResidualDownBlock = WanDistResidualDownBlock
ResidualBlock = WanDistResidualBlock
AttentionBlock = WanDistAttentionBlock
@@ -488,6 +1046,9 @@ class WanResidualUpBlock(nn.Module):
temperal_upsample: bool = False,
up_flag: bool = False,
non_linearity: str = "silu",
*,
residual_block_cls=WanResidualBlock,
resample_cls=WanResample,
):
super().__init__()
self.in_dim = in_dim
@@ -508,7 +1069,7 @@ class WanResidualUpBlock(nn.Module):
current_dim = in_dim
for _ in range(num_res_blocks + 1):
resnets.append(
WanResidualBlock(current_dim, out_dim, dropout, non_linearity)
residual_block_cls(current_dim, out_dim, dropout, non_linearity)
)
current_dim = out_dim
@@ -517,7 +1078,7 @@ class WanResidualUpBlock(nn.Module):
# Add upsampling layer if needed
if up_flag:
upsample_mode = "upsample3d" if temperal_upsample else "upsample2d"
self.upsampler = WanResample(
self.upsampler = resample_cls(
out_dim, mode=upsample_mode, upsample_out_dim=out_dim
)
else:
@@ -550,6 +1111,9 @@ class WanUpBlock(nn.Module):
dropout: float = 0.0,
upsample_mode: str | None = None,
non_linearity: str = "silu",
*,
residual_block_cls=WanResidualBlock,
resample_cls=WanResample,
):
super().__init__()
self.in_dim = in_dim
@@ -561,7 +1125,7 @@ class WanUpBlock(nn.Module):
current_dim = in_dim
for _ in range(num_res_blocks + 1):
resnets.append(
WanResidualBlock(current_dim, out_dim, dropout, non_linearity)
residual_block_cls(current_dim, out_dim, dropout, non_linearity)
)
current_dim = out_dim
@@ -570,7 +1134,7 @@ class WanUpBlock(nn.Module):
# Add upsampling layer if needed
self.upsamplers = None
if upsample_mode is not None:
self.upsamplers = nn.ModuleList([WanResample(out_dim, mode=upsample_mode)])
self.upsamplers = nn.ModuleList([resample_cls(out_dim, mode=upsample_mode)])
self.gradient_checkpointing = False
@@ -578,6 +1142,52 @@ class WanUpBlock(nn.Module):
return up_block_forward(self, x)
class WanDistResidualUpBlock(WanResidualUpBlock):
def __init__(
self,
in_dim: int,
out_dim: int,
num_res_blocks: int,
dropout: float = 0.0,
temperal_upsample: bool = False,
up_flag: bool = False,
non_linearity: str = "silu",
):
super().__init__(
in_dim,
out_dim,
num_res_blocks,
dropout=dropout,
temperal_upsample=temperal_upsample,
up_flag=up_flag,
non_linearity=non_linearity,
residual_block_cls=WanDistResidualBlock,
resample_cls=WanDistResample,
)
class WanDistUpBlock(WanUpBlock):
def __init__(
self,
in_dim: int,
out_dim: int,
num_res_blocks: int,
dropout: float = 0.0,
upsample_mode: str | None = None,
non_linearity: str = "silu",
):
super().__init__(
in_dim,
out_dim,
num_res_blocks,
dropout=dropout,
upsample_mode=upsample_mode,
non_linearity=non_linearity,
residual_block_cls=WanDistResidualBlock,
resample_cls=WanDistResample,
)
class WanDecoder3d(nn.Module):
r"""
A 3D decoder module.
@@ -628,7 +1238,7 @@ class WanDecoder3d(nn.Module):
world_size = get_decode_parallel_world_size()
if use_parallel_decode and world_size > 1:
CausalConv3d = WanDistCausalConv3d
CausalConv3d = SpatialParallelCausalConv3d
MidBlock = WanDistMidBlock
ResidualUpBlock = WanDistResidualUpBlock
UpBlock = WanDistUpBlock
@@ -861,18 +1471,21 @@ class AutoencoderKLWan(ParallelTiledVAE):
dropout=config.dropout,
out_channels=config.out_channels,
is_residual=config.is_residual,
use_parallel_decode=self.use_parallel_decode,
use_parallel_decode=should_use_spatial_shard_parallel_decode(config),
)
self.use_feature_cache = config.use_feature_cache
self._causal_decode_initialized = False
def _should_use_spatial_parallel_decode(self, z: torch.Tensor) -> bool:
return should_run_spatial_shard_parallel_decode(self.config, z)
def clear_cache(self) -> None:
def _count_conv3d(model) -> int:
count = 0
for m in model.modules():
if isinstance(m, WanCausalConv3d) or isinstance(m, WanDistCausalConv3d):
if isinstance(m, (WanCausalConv3d, SpatialParallelCausalConv3d)):
count += 1
return count
@@ -904,13 +1517,19 @@ class AutoencoderKLWan(ParallelTiledVAE):
iter_ = z.shape[2]
x = self.post_quant_conv(z)
outs = []
with forward_context(
feat_cache_arg=self._feat_map, feat_idx_arg=self._conv_idx
):
for i in range(iter_):
feat_idx.set(0)
first_chunk.set(is_first_chunk and i == 0)
outs.append(self.decoder(x[:, :, i : i + 1, :, :]))
spatial_context = (
nullcontext()
if self._should_use_spatial_parallel_decode(z)
else disable_spatial_parallel_decode()
)
with spatial_context:
with forward_context(
feat_cache_arg=self._feat_map, feat_idx_arg=self._conv_idx
):
for i in range(iter_):
feat_idx.set(0)
first_chunk.set(is_first_chunk and i == 0)
outs.append(self.decoder(x[:, :, i : i + 1, :, :]))
out = torch.cat(outs, 2)
if self.config.patch_size is not None:
@@ -984,16 +1603,25 @@ class AutoencoderKLWan(ParallelTiledVAE):
self.clear_cache()
iter_ = z.shape[2]
x = self.post_quant_conv(z)
outs = []
with forward_context(
feat_cache_arg=self._feat_map, feat_idx_arg=self._conv_idx
):
out_chunks = []
for i in range(iter_):
feat_idx.set(0)
first_chunk.set(i == 0)
out_chunks.append(self.decoder(x[:, :, i : i + 1, :, :]))
out = torch.cat(out_chunks, 2) if len(out_chunks) > 1 else out_chunks[0]
spatial_context = (
nullcontext()
if self._should_use_spatial_parallel_decode(z)
else disable_spatial_parallel_decode()
)
with spatial_context:
with forward_context(
feat_cache_arg=self._feat_map, feat_idx_arg=self._conv_idx
):
out_chunks = []
for i in range(iter_):
feat_idx.set(0)
first_chunk.set(i == 0)
out_chunks.append(self.decoder(x[:, :, i : i + 1, :, :]))
out = (
torch.cat(out_chunks, 2)
if len(out_chunks) > 1
else out_chunks[0]
)
if self.config.patch_size is not None:
out = unpatchify(out, patch_size=self.config.patch_size)
@@ -1008,8 +1636,14 @@ class AutoencoderKLWan(ParallelTiledVAE):
def _decode(self, z: torch.Tensor, first_frame=False) -> torch.Tensor:
x = self.post_quant_conv(z)
with forward_context(first_frame_arg=first_frame):
out = self.decoder(x)
spatial_context = (
nullcontext()
if self._should_use_spatial_parallel_decode(z)
else disable_spatial_parallel_decode()
)
with spatial_context:
with forward_context(first_frame_arg=first_frame):
out = self.decoder(x)
out = torch.clamp(out, min=-1.0, max=1.0)
@@ -2,6 +2,10 @@ import torch
from diffusers.utils.torch_utils import randn_tensor
from sglang.multimodal_gen.configs.pipeline_configs.ltx_2 import is_ltx23_native_variant
from sglang.multimodal_gen.runtime.distributed import (
get_decode_parallel_world_size,
model_parallel_is_initialized,
)
from sglang.multimodal_gen.runtime.managers.memory_managers.component_manager import (
ComponentUse,
)
@@ -146,8 +150,15 @@ class LTX2RefinementStage(LTX2AVDenoisingStage):
@property
def parallelism_type(self) -> StageParallelismType:
# Stage 2 is distilled and always runs with CFG disabled, so non-main
# CFG ranks should wait at a barrier rather than run a redundant forward.
# CFG ranks only need the result when the decoder will use all ranks.
if self.server_args.enable_cfg_parallel:
if (
model_parallel_is_initialized()
and get_decode_parallel_world_size() > 1
and self.vae is not None
and self.vae.use_parallel_decode
):
return StageParallelismType.MAIN_RANK_ONLY_AND_SEND_TO_OTHERS
return StageParallelismType.MAIN_RANK_ONLY
return StageParallelismType.REPLICATED
@@ -34,7 +34,7 @@ if TYPE_CHECKING:
logger = init_logger(__name__)
SGL_TEST_FILES_CI_DATA_REVISION = "caa56302ccf2d289e4488ed06d952edf5d2314cf"
SGL_TEST_FILES_CI_DATA_REVISION = "f0fd96eab85baed5256d142c7659e0634e7e4410"
if current_platform.is_npu():
SGL_TEST_FILES_CI_DATA_REVISION = "670d66a8a290b62c0c3c077b3e9b0f4a4d9a44e7"
@@ -6,6 +6,9 @@ from sglang.multimodal_gen.runtime.pipelines_core.stages.base import (
StageParallelismType,
)
from sglang.multimodal_gen.runtime.pipelines_core.stages.decoding import DecodingStage
from sglang.multimodal_gen.runtime.pipelines_core.stages.model_specific_stages.ltx_2.denoising_av import (
LTX2RefinementStage,
)
class TestDecodingStageParallelism(unittest.TestCase):
@@ -95,6 +98,46 @@ class TestDecodingStageParallelism(unittest.TestCase):
StageParallelismType.REPLICATED,
)
def test_ltx2_refinement_broadcasts_when_decode_uses_all_ranks(self):
stage = object.__new__(LTX2RefinementStage)
stage.server_args = SimpleNamespace(enable_cfg_parallel=True)
stage.vae = SimpleNamespace(use_parallel_decode=True)
with (
patch(
"sglang.multimodal_gen.runtime.pipelines_core.stages.model_specific_stages.ltx_2.denoising_av.model_parallel_is_initialized",
return_value=True,
),
patch(
"sglang.multimodal_gen.runtime.pipelines_core.stages.model_specific_stages.ltx_2.denoising_av.get_decode_parallel_world_size",
return_value=2,
),
):
self.assertEqual(
stage.parallelism_type,
StageParallelismType.MAIN_RANK_ONLY_AND_SEND_TO_OTHERS,
)
def test_ltx2_refinement_stays_main_rank_only_without_parallel_decode(self):
stage = object.__new__(LTX2RefinementStage)
stage.server_args = SimpleNamespace(enable_cfg_parallel=True)
stage.vae = SimpleNamespace(use_parallel_decode=False)
with (
patch(
"sglang.multimodal_gen.runtime.pipelines_core.stages.model_specific_stages.ltx_2.denoising_av.model_parallel_is_initialized",
return_value=True,
),
patch(
"sglang.multimodal_gen.runtime.pipelines_core.stages.model_specific_stages.ltx_2.denoising_av.get_decode_parallel_world_size",
return_value=2,
),
):
self.assertEqual(
stage.parallelism_type,
StageParallelismType.MAIN_RANK_ONLY,
)
if __name__ == "__main__":
unittest.main()
@@ -17,7 +17,7 @@ from sglang.multimodal_gen.runtime.loader.component_loaders.vae_loader import (
_backfill_ltx2_audio_vae_latent_stats,
_should_use_channels_last_3d,
)
from sglang.multimodal_gen.runtime.models.vaes.parallel import wan_common_utils
from sglang.multimodal_gen.runtime.models.vaes import wanvae
class _FakeServerArgs:
@@ -176,14 +176,10 @@ class TestVAELoader(unittest.TestCase):
)
with (
patch.object(
wan_common_utils.current_platform, "is_cuda", return_value=False
),
patch.object(
wan_common_utils.current_platform, "is_rocm", return_value=False
),
patch.object(wanvae.current_platform, "is_cuda", return_value=False),
patch.object(wanvae.current_platform, "is_rocm", return_value=False),
):
out = wan_common_utils.match_conv3d_input_format(x, weight)
out = wanvae.match_conv3d_input_format(x, weight)
self.assertIs(out, x)
@@ -197,14 +193,10 @@ class TestVAELoader(unittest.TestCase):
)
with (
patch.object(
wan_common_utils.current_platform, "is_cuda", return_value=True
),
patch.object(
wan_common_utils.current_platform, "is_rocm", return_value=False
),
patch.object(wanvae.current_platform, "is_cuda", return_value=True),
patch.object(wanvae.current_platform, "is_rocm", return_value=False),
):
out = wan_common_utils.match_conv3d_input_format(x, weight)
out = wanvae.match_conv3d_input_format(x, weight)
self.assertTrue(out.is_contiguous(memory_format=torch.channels_last_3d))
@@ -0,0 +1,512 @@
import unittest
from types import SimpleNamespace
from unittest.mock import patch
import torch
from sglang.multimodal_gen.configs.models.vaes.base import (
VAEConfig,
is_spatial_shard_parallel_decode_mode,
should_use_spatial_shard_parallel_decode,
)
from sglang.multimodal_gen.configs.models.vaes.ernie_image import ErnieImageVAEConfig
from sglang.multimodal_gen.configs.models.vaes.flux import Flux2VAEConfig, FluxVAEConfig
from sglang.multimodal_gen.configs.models.vaes.glmimage import GlmImageVAEConfig
from sglang.multimodal_gen.configs.models.vaes.hunyuan3d import Hunyuan3DVAEConfig
from sglang.multimodal_gen.configs.models.vaes.hunyuanvae import HunyuanVAEConfig
from sglang.multimodal_gen.configs.models.vaes.ltx_audio import LTXAudioVAEConfig
from sglang.multimodal_gen.configs.models.vaes.ltx_video import LTXVideoVAEConfig
from sglang.multimodal_gen.configs.models.vaes.qwenimage import QwenImageVAEConfig
from sglang.multimodal_gen.configs.models.vaes.sana import SanaVAEConfig
from sglang.multimodal_gen.configs.models.vaes.stablediffusion3 import (
StableDiffusion3VAEConfig,
)
from sglang.multimodal_gen.configs.models.vaes.wanvae import WanVAEConfig
from sglang.multimodal_gen.configs.utils import update_config_from_args
from sglang.multimodal_gen.runtime.distributed import parallel_state
from sglang.multimodal_gen.runtime.layers.parallel_conv import (
SpatialParallelCausalConv3d,
SpatialParallelConv2d,
SpatialParallelConv3d,
chunk_height_by_sizes,
split_for_parallel_decode,
)
from sglang.multimodal_gen.runtime.models.vaes.autoencoder import AutoencoderKL
from sglang.multimodal_gen.runtime.models.vaes.autoencoder_kl_qwenimage import (
QwenImageAttentionBlock,
QwenImageDecoder3d,
)
from sglang.multimodal_gen.runtime.models.vaes.common import ParallelTiledVAE
from sglang.multimodal_gen.runtime.models.vaes.hunyuanvae import (
HunyuanVideoDecoder3D,
HunyuanVideoMidBlock3D,
HunyuanVideoResnetBlockCausal3D,
_enable_hunyuan_decoder_spatial_parallel,
)
from sglang.multimodal_gen.runtime.models.vaes.ltx_2_vae import (
LTX2VideoCausalConv3d,
LTX2VideoDecoder3d,
_enable_ltx_decoder_spatial_parallel,
)
from sglang.multimodal_gen.runtime.models.vaes.wanvae import (
WanDecoder3d,
WanDistAttentionBlock,
)
from sglang.multimodal_gen.runtime.utils.distributed import RankGenerator
from sglang.multimodal_gen.utils import FlexibleArgumentParser
class _DispatchProbeVAE(ParallelTiledVAE):
def __init__(self, config):
super().__init__(config)
self.used_decode = False
self.used_parallel_tiled_decode = False
def _encode(self, x):
return x
def _decode(self, z):
self.used_decode = True
return z
def parallel_tiled_decode(self, z):
self.used_parallel_tiled_decode = True
raise AssertionError(
"spatial_shard decode should not use parallel_tiled_decode"
)
class TestVAESpatialParallelDecode(unittest.TestCase):
def test_base_vae_config_defaults_to_auto_parallel_decode(self):
config = VAEConfig()
self.assertTrue(config.use_parallel_decode)
self.assertEqual(config.parallel_decode_mode, "auto")
def test_image_video_vae_configs_default_to_auto_parallel_decode(self):
configs = (
ErnieImageVAEConfig(),
FluxVAEConfig(),
Flux2VAEConfig(),
GlmImageVAEConfig(),
HunyuanVAEConfig(),
LTXVideoVAEConfig(),
QwenImageVAEConfig(),
SanaVAEConfig(),
StableDiffusion3VAEConfig(),
WanVAEConfig(),
)
for config in configs:
with self.subTest(config=type(config).__name__):
self.assertTrue(config.use_parallel_decode)
self.assertEqual(config.parallel_decode_mode, "auto")
def test_auto_parallel_decode_policy_is_conservative(self):
self.assertFalse(is_spatial_shard_parallel_decode_mode("auto"))
self.assertFalse(should_use_spatial_shard_parallel_decode(VAEConfig()))
self.assertFalse(should_use_spatial_shard_parallel_decode(QwenImageVAEConfig()))
self.assertTrue(should_use_spatial_shard_parallel_decode(LTXVideoVAEConfig()))
self.assertTrue(should_use_spatial_shard_parallel_decode(WanVAEConfig()))
ltx23_config = LTXVideoVAEConfig()
ltx23_config.arch_config.video_decoder_variant = "ltx_2_3"
self.assertTrue(should_use_spatial_shard_parallel_decode(ltx23_config))
ltx23_config.parallel_decode_mode = "spatial_shard"
self.assertTrue(should_use_spatial_shard_parallel_decode(ltx23_config))
config = QwenImageVAEConfig()
self.assertFalse(
should_use_spatial_shard_parallel_decode(
config, torch.empty(1, 16, 1, 128, 128), 2
)
)
config.parallel_decode_mode = "spatial_shard"
self.assertTrue(should_use_spatial_shard_parallel_decode(config))
hunyuan_config = HunyuanVAEConfig()
self.assertTrue(should_use_spatial_shard_parallel_decode(hunyuan_config))
self.assertFalse(
should_use_spatial_shard_parallel_decode(
hunyuan_config, torch.empty(1, 16, 9, 16, 16), 2
)
)
self.assertTrue(
should_use_spatial_shard_parallel_decode(
hunyuan_config, torch.empty(1, 16, 9, 96, 96), 2
)
)
def test_unsupported_vae_configs_opt_out_of_spatial_parallel_decode(self):
configs = (Hunyuan3DVAEConfig(), LTXAudioVAEConfig())
for config in configs:
with self.subTest(config=type(config).__name__):
self.assertFalse(config.use_parallel_decode)
self.assertEqual(config.parallel_decode_mode, "tiled")
def test_vae_nested_cli_defaults_do_not_override_model_defaults(self):
parser = FlexibleArgumentParser()
VAEConfig.add_cli_args(parser)
parsed = vars(parser.parse_args([]))
config = FluxVAEConfig()
update_config_from_args(config, parsed, "vae_config")
self.assertTrue(config.use_parallel_decode)
self.assertEqual(config.parallel_decode_mode, "auto")
def test_vae_nested_cli_explicit_args_override_model_defaults(self):
parser = FlexibleArgumentParser()
VAEConfig.add_cli_args(parser)
parsed = vars(
parser.parse_args(
[
"--vae-config.use-parallel-decode",
"false",
"--vae-config.parallel-decode-mode",
"patch",
]
)
)
config = FluxVAEConfig()
update_config_from_args(config, parsed, "vae_config")
self.assertFalse(config.use_parallel_decode)
self.assertEqual(config.parallel_decode_mode, "patch")
def test_base_decode_prefers_spatial_parallel_dispatch(self):
config = VAEConfig()
config.arch_config.temporal_compression_ratio = 1
config.arch_config.spatial_compression_ratio = 1
config.use_parallel_decode = True
config.parallel_decode_mode = "spatial_shard"
vae = _DispatchProbeVAE(config)
z = torch.randn(1, 1, 1, 2, 2)
with (
patch(
"sglang.multimodal_gen.runtime.models.vaes.common.dist.is_initialized",
return_value=True,
),
patch(
"sglang.multimodal_gen.runtime.models.vaes.common.model_parallel_is_initialized",
return_value=True,
),
patch(
"sglang.multimodal_gen.runtime.models.vaes.common.get_decode_parallel_group_coordinator",
return_value=SimpleNamespace(),
),
patch(
"sglang.multimodal_gen.runtime.models.vaes.common.get_decode_parallel_world_size",
return_value=2,
),
):
out = vae.decode(z)
self.assertTrue(vae.used_decode)
self.assertFalse(vae.used_parallel_tiled_decode)
torch.testing.assert_close(out, z)
def test_auto_decode_does_not_fall_back_to_parallel_tiling(self):
config = VAEConfig()
config.arch_config.temporal_compression_ratio = 1
config.arch_config.spatial_compression_ratio = 1
config.use_parallel_decode = True
config.parallel_decode_mode = "auto"
vae = _DispatchProbeVAE(config)
z = torch.randn(1, 1, 1, 2, 2)
with patch(
"sglang.multimodal_gen.runtime.models.vaes.common.get_sp_world_size",
return_value=2,
):
out = vae.decode(z)
self.assertTrue(vae.used_decode)
self.assertFalse(vae.used_parallel_tiled_decode)
torch.testing.assert_close(out, z)
def test_spatial_alias_still_uses_spatial_shard_dispatch(self):
self.assertTrue(is_spatial_shard_parallel_decode_mode("spatial"))
def test_decode_parallel_group_uses_dedicated_group(self):
old_decode = parallel_state._VAE_DECODE
decode_group = SimpleNamespace(world_size=4, rank_in_group=2)
try:
parallel_state._VAE_DECODE = decode_group
self.assertIs(
parallel_state.get_decode_parallel_group_coordinator(), decode_group
)
self.assertEqual(parallel_state.get_decode_parallel_world_size(), 4)
self.assertEqual(parallel_state.get_decode_parallel_rank(), 2)
finally:
parallel_state._VAE_DECODE = old_decode
def test_decode_rank_groups_cover_non_dp_parallel_axes(self):
rank_generator = RankGenerator(
tp=2,
sp=2,
pp=1,
cfg=2,
dp=2,
order="tp-sp-pp-cfg-dp",
)
self.assertEqual(
rank_generator.get_ranks("tp-sp-pp-cfg"),
[list(range(0, 8)), list(range(8, 16))],
)
def test_spatial_split_keeps_uneven_height_lossless(self):
x = torch.arange(5).view(1, 1, 1, 5, 1)
rank0, expected_height = split_for_parallel_decode(
x, upsample_count=1, world_size=2, rank=0
)
rank1, _ = split_for_parallel_decode(x, upsample_count=1, world_size=2, rank=1)
self.assertEqual(expected_height, 10)
self.assertEqual(rank0.shape[-2], 3)
self.assertEqual(rank1.shape[-2], 2)
torch.testing.assert_close(torch.cat([rank0, rank1], dim=-2), x)
def test_chunk_height_by_sizes_keeps_original_partition(self):
x = torch.arange(10).view(1, 1, 10, 1)
with patch(
"sglang.multimodal_gen.runtime.layers.parallel_conv.get_decode_parallel_rank",
return_value=0,
):
rank0 = chunk_height_by_sizes(x, [6, 4])
with patch(
"sglang.multimodal_gen.runtime.layers.parallel_conv.get_decode_parallel_rank",
return_value=1,
):
rank1 = chunk_height_by_sizes(x, [6, 4])
self.assertEqual(rank0.shape[-2], 6)
self.assertEqual(rank1.shape[-2], 4)
torch.testing.assert_close(torch.cat([rank0, rank1], dim=-2), x)
def test_qwen_decoder_uses_spatial_parallel_components(self):
with (
patch(
"sglang.multimodal_gen.runtime.models.vaes.autoencoder_kl_qwenimage.dist.is_initialized",
return_value=True,
),
patch(
"sglang.multimodal_gen.runtime.models.vaes.autoencoder_kl_qwenimage.model_parallel_is_initialized",
return_value=True,
),
patch(
"sglang.multimodal_gen.runtime.models.vaes.autoencoder_kl_qwenimage.get_decode_parallel_world_size",
return_value=2,
),
patch(
"sglang.multimodal_gen.runtime.models.vaes.autoencoder_kl_qwenimage.get_decode_parallel_rank",
return_value=0,
),
patch(
"sglang.multimodal_gen.runtime.layers.parallel_conv.get_decode_parallel_world_size",
return_value=2,
),
patch(
"sglang.multimodal_gen.runtime.layers.parallel_conv.get_decode_parallel_rank",
return_value=0,
),
):
decoder = QwenImageDecoder3d(
dim=4,
z_dim=2,
dim_mult=(1, 1),
num_res_blocks=1,
attn_scales=(),
temperal_upsample=(False,),
input_channels=3,
use_parallel_decode=True,
)
self.assertTrue(
any(isinstance(m, SpatialParallelCausalConv3d) for m in decoder.modules())
)
self.assertTrue(
any(
isinstance(m, QwenImageAttentionBlock) and m.spatial_parallel
for m in decoder.modules()
)
)
def test_wan_decoder_uses_spatial_parallel_components(self):
with (
patch(
"sglang.multimodal_gen.runtime.models.vaes.wanvae.dist.is_initialized",
return_value=True,
),
patch(
"sglang.multimodal_gen.runtime.models.vaes.wanvae.get_decode_parallel_world_size",
return_value=2,
),
patch(
"sglang.multimodal_gen.runtime.models.vaes.wanvae.get_decode_parallel_rank",
return_value=0,
),
patch(
"sglang.multimodal_gen.runtime.layers.parallel_conv.get_decode_parallel_world_size",
return_value=2,
),
patch(
"sglang.multimodal_gen.runtime.layers.parallel_conv.get_decode_parallel_rank",
return_value=0,
),
):
decoder = WanDecoder3d(
dim=4,
z_dim=2,
dim_mult=(1, 1),
num_res_blocks=1,
attn_scales=(),
temperal_upsample=(False,),
out_channels=3,
use_parallel_decode=True,
)
self.assertTrue(
any(isinstance(m, SpatialParallelCausalConv3d) for m in decoder.modules())
)
self.assertTrue(
any(isinstance(m, WanDistAttentionBlock) for m in decoder.modules())
)
def test_diffusers_2d_decoder_uses_spatial_parallel_components(self):
config = StableDiffusion3VAEConfig()
config.use_parallel_decode = True
config.parallel_decode_mode = "spatial_shard"
config.arch_config.latent_channels = 2
config.arch_config.block_out_channels = (4, 4)
config.arch_config.down_block_types = (
"DownEncoderBlock2D",
"DownEncoderBlock2D",
)
config.arch_config.up_block_types = (
"UpDecoderBlock2D",
"UpDecoderBlock2D",
)
config.arch_config.layers_per_block = 1
config.arch_config.norm_num_groups = 1
config.arch_config.sample_size = 8
with (
patch(
"sglang.multimodal_gen.runtime.models.vaes.common.dist.is_initialized",
return_value=True,
),
patch(
"sglang.multimodal_gen.runtime.models.vaes.common.model_parallel_is_initialized",
return_value=True,
),
patch(
"sglang.multimodal_gen.runtime.models.vaes.common.get_decode_parallel_group_coordinator",
return_value=SimpleNamespace(),
),
patch(
"sglang.multimodal_gen.runtime.models.vaes.common.get_decode_parallel_world_size",
return_value=2,
),
patch(
"sglang.multimodal_gen.runtime.layers.parallel_conv.get_decode_parallel_world_size",
return_value=2,
),
patch(
"sglang.multimodal_gen.runtime.layers.parallel_conv.get_decode_parallel_rank",
return_value=0,
),
):
vae = AutoencoderKL(config)
self.assertTrue(vae._spatial_parallel_decode_enabled)
self.assertTrue(
any(isinstance(m, SpatialParallelConv2d) for m in vae.decoder.modules())
)
def test_ltx_decoder_uses_spatial_parallel_conv3d(self):
with (
patch(
"sglang.multimodal_gen.runtime.layers.parallel_conv.get_decode_parallel_world_size",
return_value=2,
),
patch(
"sglang.multimodal_gen.runtime.layers.parallel_conv.get_decode_parallel_rank",
return_value=0,
),
):
decoder = LTX2VideoDecoder3d(
in_channels=4,
out_channels=3,
block_out_channels=(8,),
spatio_temporal_scaling=(False,),
layers_per_block=(1, 1),
patch_size=1,
upsample_residual=(False,),
upsample_factor=(1,),
spatial_padding_mode="reflect",
)
_enable_ltx_decoder_spatial_parallel(decoder)
causal_convs = [
m for m in decoder.modules() if isinstance(m, LTX2VideoCausalConv3d)
]
self.assertGreater(len(causal_convs), 0)
self.assertTrue(
all(isinstance(m.conv, SpatialParallelConv3d) for m in causal_convs)
)
def test_hunyuan_decoder_uses_spatial_parallel_components(self):
with (
patch(
"sglang.multimodal_gen.runtime.layers.parallel_conv.get_decode_parallel_world_size",
return_value=2,
),
patch(
"sglang.multimodal_gen.runtime.layers.parallel_conv.get_decode_parallel_rank",
return_value=0,
),
):
decoder = HunyuanVideoDecoder3D(
in_channels=4,
out_channels=3,
up_block_types=("HunyuanVideoUpBlock3D", "HunyuanVideoUpBlock3D"),
block_out_channels=(4, 8),
layers_per_block=1,
norm_num_groups=1,
mid_block_add_attention=True,
time_compression_ratio=4,
spatial_compression_ratio=2,
)
_enable_hunyuan_decoder_spatial_parallel(decoder)
self.assertTrue(decoder.spatial_parallel)
self.assertTrue(
any(
isinstance(m, HunyuanVideoMidBlock3D) and m.spatial_parallel
for m in decoder.modules()
)
)
self.assertTrue(
any(isinstance(m, SpatialParallelConv3d) for m in decoder.modules())
)
shortcut_convs = [
m.conv_shortcut.conv
for m in decoder.modules()
if isinstance(m, HunyuanVideoResnetBlockCausal3D)
and m.conv_shortcut is not None
]
self.assertTrue(shortcut_convs)
self.assertTrue(
all(not isinstance(conv, SpatialParallelConv3d) for conv in shortcut_convs)
)
if __name__ == "__main__":
unittest.main()