Signed-off-by: Maciej Bala <mbala@nvidia.com> Signed-off-by: MaciejBalaNV <mbala@nvidia.com>
620 lines
28 KiB
Python
620 lines
28 KiB
Python
# SPDX-License-Identifier: Apache-2.0
|
|
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
|
"""CPU tests for the Wan VAE decoder fast path installer and its exact PyTorch fallbacks."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import importlib
|
|
from functools import wraps
|
|
from types import SimpleNamespace
|
|
|
|
import pytest
|
|
import torch
|
|
from diffusers.models.autoencoders import AutoencoderKLWan
|
|
from torch import nn
|
|
|
|
from vllm_omni.diffusion import registry as registry_module
|
|
from vllm_omni.diffusion.data import OmniDiffusionConfig
|
|
from vllm_omni.diffusion.distributed.autoencoders.autoencoder_kl_wan import OmniAutoencoderKLWan
|
|
from vllm_omni.diffusion.distributed.autoencoders.wan_vae_fastpath import (
|
|
REPORT_ATTR,
|
|
decode_frames,
|
|
install_wan_vae_fastpath,
|
|
is_installed,
|
|
uninstall_wan_vae_fastpath,
|
|
)
|
|
from vllm_omni.diffusion.distributed.autoencoders.wan_vae_fastpath import forwards as fastpath_forwards
|
|
|
|
pytestmark = [pytest.mark.core_model, pytest.mark.cpu, pytest.mark.diffusion]
|
|
|
|
TINY_RESIDUAL = dict(
|
|
base_dim=8,
|
|
decoder_base_dim=8,
|
|
z_dim=4,
|
|
dim_mult=[1, 1],
|
|
num_res_blocks=1,
|
|
temperal_downsample=[False, True],
|
|
is_residual=True,
|
|
)
|
|
TINY_RESIDUAL_PATCH2 = dict(TINY_RESIDUAL, patch_size=2, in_channels=12, out_channels=12)
|
|
TINY_WAN21 = dict(
|
|
base_dim=8,
|
|
z_dim=4,
|
|
dim_mult=[1, 2],
|
|
num_res_blocks=1,
|
|
temperal_downsample=[False, True],
|
|
is_residual=False,
|
|
)
|
|
CONFIGS = {
|
|
"residual": TINY_RESIDUAL,
|
|
"residual_patch2": TINY_RESIDUAL_PATCH2,
|
|
"wan21": TINY_WAN21,
|
|
}
|
|
|
|
|
|
def _build_pair(config: dict, dtype: torch.dtype) -> tuple[AutoencoderKLWan, AutoencoderKLWan]:
|
|
torch.manual_seed(0)
|
|
reference = AutoencoderKLWan(**config).eval().to(dtype)
|
|
candidate = AutoencoderKLWan(**config).eval().to(dtype)
|
|
candidate.load_state_dict(reference.state_dict())
|
|
return reference, candidate
|
|
|
|
|
|
@pytest.mark.parametrize("config_name", sorted(CONFIGS))
|
|
@pytest.mark.parametrize("dtype", [torch.float32, torch.bfloat16])
|
|
@pytest.mark.parametrize("frames", [1, 3])
|
|
def test_lossless_fallback_paths_are_bitwise_exact(config_name: str, dtype: torch.dtype, frames: int) -> None:
|
|
"""Without CUDA every kernel declines, so this exercises the restructured PyTorch paths."""
|
|
config = CONFIGS[config_name]
|
|
reference, candidate = _build_pair(config, dtype)
|
|
report = install_wan_vae_fastpath(candidate, level="lossless")
|
|
assert report.installed, report
|
|
assert report.fused_silu_dtypes == ()
|
|
|
|
torch.manual_seed(1)
|
|
latents = torch.randn(1, config["z_dim"], frames, 6, 8).to(dtype)
|
|
with torch.no_grad():
|
|
expected = reference.decode(latents, return_dict=False)[0]
|
|
actual = candidate.decode(latents, return_dict=False)[0]
|
|
assert actual.stride() == expected.stride()
|
|
assert torch.equal(actual, expected)
|
|
|
|
|
|
@pytest.mark.parametrize("config_name", sorted(CONFIGS))
|
|
@pytest.mark.parametrize("dtype", [torch.float32, torch.bfloat16])
|
|
def test_decode_frames_matches_reference_decode(config_name: str, dtype: torch.dtype) -> None:
|
|
config = CONFIGS[config_name]
|
|
reference, candidate = _build_pair(config, dtype)
|
|
install_wan_vae_fastpath(candidate)
|
|
for frames in (1, 2, 4):
|
|
torch.manual_seed(2)
|
|
latents = torch.randn(1, config["z_dim"], frames, 6, 8).to(dtype)
|
|
with torch.no_grad():
|
|
expected = reference._decode(latents, return_dict=False)[0]
|
|
actual = decode_frames(candidate, latents)
|
|
assert actual.stride() == expected.stride()
|
|
assert torch.equal(actual, expected)
|
|
|
|
|
|
@pytest.mark.parametrize("dtype", [torch.float32, torch.bfloat16])
|
|
def test_omni_wan_vae_decode_override_is_bitwise_exact(dtype: torch.dtype) -> None:
|
|
torch.manual_seed(0)
|
|
reference = AutoencoderKLWan(**TINY_RESIDUAL_PATCH2).eval().to(dtype)
|
|
candidate = OmniAutoencoderKLWan(**TINY_RESIDUAL_PATCH2).eval().to(dtype)
|
|
candidate.load_state_dict(reference.state_dict())
|
|
latents = torch.randn(1, 4, 3, 6, 8).to(dtype)
|
|
with torch.no_grad():
|
|
expected = reference.decode(latents, return_dict=False)[0]
|
|
before_install = candidate.decode(latents, return_dict=False)[0]
|
|
install_wan_vae_fastpath(candidate)
|
|
actual = candidate.decode(latents, return_dict=False)[0]
|
|
as_dict = candidate.decode(latents).sample
|
|
# The Omni wrapper runs bf16 decode under autocast (also on CPU), so compare
|
|
# the override against the same wrapper before installation, and against the
|
|
# plain diffusers decode only where no autocast is involved.
|
|
assert actual.stride() == before_install.stride()
|
|
assert torch.equal(actual, before_install)
|
|
assert torch.equal(as_dict, before_install)
|
|
if dtype is torch.float32:
|
|
assert torch.equal(before_install, expected)
|
|
|
|
# Tiling still dispatches to the diffusers implementation.
|
|
candidate.use_tiling = True
|
|
candidate.tile_sample_min_height = candidate.tile_sample_min_width = 16
|
|
candidate.tile_sample_stride_height = candidate.tile_sample_stride_width = 16
|
|
reference.use_tiling = True
|
|
reference.tile_sample_min_height = reference.tile_sample_min_width = 16
|
|
reference.tile_sample_stride_height = reference.tile_sample_stride_width = 16
|
|
with torch.no_grad():
|
|
expected_tiled = reference.decode(latents, return_dict=False)[0]
|
|
actual_tiled = candidate.decode(latents, return_dict=False)[0]
|
|
if dtype is torch.float32:
|
|
assert torch.equal(actual_tiled, expected_tiled)
|
|
assert actual_tiled.shape == expected_tiled.shape
|
|
|
|
|
|
def test_omni_wan_vae_decode_preserves_autograd() -> None:
|
|
torch.manual_seed(0)
|
|
reference = AutoencoderKLWan(**TINY_RESIDUAL).eval()
|
|
candidate = OmniAutoencoderKLWan(**TINY_RESIDUAL).eval()
|
|
candidate.load_state_dict(reference.state_dict())
|
|
install_wan_vae_fastpath(candidate)
|
|
|
|
reference_latents = torch.randn(1, 4, 2, 6, 8, requires_grad=True)
|
|
candidate_latents = reference_latents.detach().clone().requires_grad_()
|
|
expected = reference.decode(reference_latents, return_dict=False)[0]
|
|
actual = candidate.decode(candidate_latents, return_dict=False)[0]
|
|
|
|
torch.testing.assert_close(actual, expected)
|
|
expected.square().mean().backward()
|
|
actual.square().mean().backward()
|
|
torch.testing.assert_close(candidate_latents.grad, reference_latents.grad)
|
|
|
|
|
|
def test_install_is_idempotent_and_reversible() -> None:
|
|
_, vae = _build_pair(TINY_RESIDUAL, torch.float32)
|
|
keys_before = set(vae.state_dict())
|
|
report = install_wan_vae_fastpath(vae)
|
|
assert report.installed and is_installed(vae)
|
|
assert report.patched["WanDecoder3d"] == 1
|
|
assert report.patched["WanCausalConv3d"] > 1
|
|
assert report.patched["WanRMS_norm"] > 1
|
|
assert set(vae.state_dict()) == keys_before
|
|
assert "forward" in vae.decoder.__dict__
|
|
|
|
assert install_wan_vae_fastpath(vae) is report
|
|
assert install_wan_vae_fastpath(vae, level="channels_last") is report # warns, keeps the first level
|
|
|
|
uninstall_wan_vae_fastpath(vae)
|
|
assert not is_installed(vae)
|
|
assert "forward" not in vae.decoder.__dict__
|
|
assert all("forward" not in module.__dict__ for module in vae.decoder.modules())
|
|
assert all(fastpath_forwards.CFG_ATTR not in module.__dict__ for module in vae.decoder.modules())
|
|
|
|
|
|
@torch.no_grad()
|
|
@pytest.mark.parametrize("failure_stage", ["binding", "conversion", "report"])
|
|
def test_failed_install_restores_original_state(failure_stage: str, monkeypatch) -> None:
|
|
installer = importlib.import_module(install_wan_vae_fastpath.__module__)
|
|
_, vae = _build_pair(TINY_RESIDUAL, torch.float32)
|
|
# Preserve mixed original layouts, an existing forward, gradients and a
|
|
# buffer: Module.to can replace tensors as well as change their storage.
|
|
vae.decoder.conv_in.to(memory_format=torch.channels_last_3d)
|
|
vae.decoder.forward = vae.decoder.forward
|
|
setattr(vae.decoder, fastpath_forwards.CFG_ATTR, object())
|
|
vae.decoder.conv_out.weight.grad = torch.randn_like(vae.decoder.conv_out.weight)
|
|
vae.post_quant_conv.register_buffer("rollback_probe", torch.randn(2, 3, 4, 5, 6))
|
|
attributes = {
|
|
module: {
|
|
name: module.__dict__[name] for name in ("forward", fastpath_forwards.CFG_ATTR) if name in module.__dict__
|
|
}
|
|
for module in vae.modules()
|
|
}
|
|
parameters = dict(vae.named_parameters())
|
|
buffers = dict(vae.named_buffers())
|
|
gradients = {name: parameter.grad for name, parameter in parameters.items()}
|
|
tensors = [*parameters.values(), *buffers.values(), *(grad for grad in gradients.values() if grad is not None)]
|
|
originals = [(tensor, tensor.data_ptr(), tensor.stride(), tensor.clone()) for tensor in tensors]
|
|
latents = torch.randn(1, 4, 2, 6, 8)
|
|
expected = vae.decode(latents, return_dict=False)[0]
|
|
|
|
with monkeypatch.context() as patch:
|
|
if failure_stage == "binding":
|
|
conv_type = type(vae.post_quant_conv)
|
|
original_setattr = conv_type.__setattr__
|
|
|
|
def fail_binding(module, name, value):
|
|
original_setattr(module, name, value)
|
|
if module is vae.post_quant_conv and name == fastpath_forwards.CFG_ATTR:
|
|
raise RuntimeError("injected installation failure")
|
|
|
|
patch.setattr(conv_type, "__setattr__", fail_binding)
|
|
elif failure_stage == "conversion":
|
|
original_to = vae.post_quant_conv.to
|
|
|
|
def fail_conversion(*args, **kwargs):
|
|
original_to(*args, **kwargs)
|
|
assert vae.decoder.conv_out.weight.is_contiguous(memory_format=torch.channels_last_3d)
|
|
raise torch.OutOfMemoryError("injected installation failure")
|
|
|
|
patch.setattr(vae.post_quant_conv, "to", fail_conversion)
|
|
else:
|
|
original_info = installer.logger.info
|
|
|
|
def fail_report(message, *args, **kwargs):
|
|
if message.startswith("Wan VAE fast path (%s) installed"):
|
|
assert is_installed(vae)
|
|
raise RuntimeError("injected installation failure")
|
|
original_info(message, *args, **kwargs)
|
|
|
|
patch.setattr(installer.logger, "info", fail_report)
|
|
|
|
with pytest.raises(RuntimeError, match="injected installation failure"):
|
|
install_wan_vae_fastpath(vae, level="channels_last")
|
|
|
|
assert not is_installed(vae)
|
|
assert not hasattr(vae, REPORT_ATTR)
|
|
assert not hasattr(vae, installer._UNDO_ATTR)
|
|
for module, original in attributes.items():
|
|
actual = {
|
|
name: module.__dict__[name] for name in ("forward", fastpath_forwards.CFG_ATTR) if name in module.__dict__
|
|
}
|
|
assert actual == original
|
|
assert all(dict(vae.named_parameters())[name] is parameter for name, parameter in parameters.items())
|
|
assert all(dict(vae.named_buffers())[name] is buffer for name, buffer in buffers.items())
|
|
assert all(parameters[name].grad is grad for name, grad in gradients.items())
|
|
for tensor, pointer, strides, values in originals:
|
|
assert tensor.data_ptr() == pointer
|
|
assert tensor.stride() == strides
|
|
assert torch.equal(tensor, values)
|
|
uninstall_wan_vae_fastpath(vae) # Safe and unnecessary after automatic rollback.
|
|
assert torch.equal(vae.decode(latents, return_dict=False)[0], expected)
|
|
assert install_wan_vae_fastpath(vae, level="channels_last").installed
|
|
uninstall_wan_vae_fastpath(vae)
|
|
|
|
|
|
def test_load_state_dict_after_install_updates_patched_modules() -> None:
|
|
reference, candidate = _build_pair(TINY_RESIDUAL, torch.float32)
|
|
install_wan_vae_fastpath(candidate)
|
|
with torch.no_grad():
|
|
for parameter in reference.parameters():
|
|
parameter.mul_(0.5)
|
|
candidate.load_state_dict(reference.state_dict())
|
|
latents = torch.randn(1, 4, 2, 6, 8)
|
|
with torch.no_grad():
|
|
expected = reference.decode(latents, return_dict=False)[0]
|
|
actual = candidate.decode(latents, return_dict=False)[0]
|
|
assert torch.equal(actual, expected)
|
|
|
|
|
|
def test_channels_last_level_converts_conv_weights_and_restores_them() -> None:
|
|
reference, vae = _build_pair(TINY_RESIDUAL, torch.float32)
|
|
report = install_wan_vae_fastpath(vae, level="channels_last")
|
|
assert report.installed and report.channels_last
|
|
conv3d = [m for m in vae.decoder.modules() if isinstance(m, nn.Conv3d)]
|
|
conv2d = [m for m in vae.decoder.modules() if isinstance(m, nn.Conv2d)]
|
|
assert conv3d and conv2d
|
|
assert all(m.weight.is_contiguous(memory_format=torch.channels_last_3d) for m in conv3d)
|
|
assert all(m.weight.is_contiguous(memory_format=torch.channels_last) for m in conv2d)
|
|
assert vae.post_quant_conv.weight.is_contiguous(memory_format=torch.channels_last_3d)
|
|
latents = torch.randn(1, 4, 2, 6, 8)
|
|
with torch.no_grad():
|
|
expected = reference.decode(latents, return_dict=False)[0]
|
|
out = vae.decode(latents, return_dict=False)[0]
|
|
# One temporal upsampler: 1 + 2 output frames for 2 latent frames.
|
|
assert expected.shape == (1, 3, 3, 12, 16)
|
|
assert out.shape == expected.shape
|
|
torch.testing.assert_close(out, expected, rtol=1e-4, atol=1e-4)
|
|
|
|
uninstall_wan_vae_fastpath(vae)
|
|
assert all(m.weight.is_contiguous() for m in conv3d + conv2d)
|
|
|
|
|
|
@torch.no_grad()
|
|
@pytest.mark.parametrize("assign", [False, True])
|
|
def test_uninstall_restores_mixed_layouts_and_keeps_weight_updates(assign: bool) -> None:
|
|
_, vae = _build_pair(TINY_RESIDUAL, torch.float32)
|
|
convs = [module for module in vae.decoder.modules() if isinstance(module, (nn.Conv2d, nn.Conv3d))]
|
|
convs.append(vae.post_quant_conv)
|
|
for index, conv in enumerate(convs):
|
|
if index % 3 == 1:
|
|
conv.to(memory_format=torch.channels_last_3d if isinstance(conv, nn.Conv3d) else torch.channels_last)
|
|
elif index % 3 == 2:
|
|
conv.weight.data = conv.weight.data.transpose(-1, -2)
|
|
vae.post_quant_conv.register_buffer("layout_probe", torch.randn(2, 3, 4, 5, 6).transpose(-1, -2))
|
|
vae.decoder.conv_out.weight.grad = torch.randn_like(vae.decoder.conv_out.weight)
|
|
grad_strides = vae.decoder.conv_out.weight.grad.stride()
|
|
strides = {name: tensor.stride() for name, tensor in vae.state_dict().items()}
|
|
# An explicit alias is safe to replace, but must remain an instance
|
|
# attribute after uninstall, with its exact original identity.
|
|
original_forward = vae.decoder.forward
|
|
vae.decoder.forward = original_forward
|
|
original_cfg = object()
|
|
setattr(vae.decoder, fastpath_forwards.CFG_ATTR, original_cfg)
|
|
|
|
assert install_wan_vae_fastpath(vae, level="channels_last").installed
|
|
updated = {name: tensor.contiguous() + 0.25 for name, tensor in vae.state_dict().items()}
|
|
vae.load_state_dict(updated, assign=assign)
|
|
uninstall_wan_vae_fastpath(vae)
|
|
|
|
assert vae.decoder.__dict__["forward"] is original_forward
|
|
assert getattr(vae.decoder, fastpath_forwards.CFG_ATTR) is original_cfg
|
|
for name, tensor in vae.state_dict().items():
|
|
assert tensor.stride() == strides[name], name
|
|
assert torch.equal(tensor, updated[name]), name
|
|
if not assign:
|
|
assert vae.decoder.conv_out.weight.grad.stride() == grad_strides
|
|
|
|
|
|
@torch.no_grad()
|
|
@pytest.mark.parametrize("level", ["lossless", "channels_last"])
|
|
@pytest.mark.parametrize(
|
|
"target",
|
|
[
|
|
"decoder",
|
|
"decoder.conv_in",
|
|
"decoder.up_blocks.0.upsampler",
|
|
"decoder.up_blocks.0.upsampler.resample",
|
|
"decoder.up_blocks.0.upsampler.resample.1",
|
|
"decoder.up_blocks.0.avg_shortcut",
|
|
"decoder.norm_out",
|
|
"decoder.nonlinearity",
|
|
"post_quant_conv",
|
|
],
|
|
)
|
|
def test_installer_declines_forward_wrappers_it_would_bypass(target: str, level: str) -> None:
|
|
_, vae = _build_pair(TINY_RESIDUAL, torch.float32)
|
|
module = vae.get_submodule(target)
|
|
original_forward = module.forward
|
|
calls = []
|
|
|
|
@wraps(original_forward)
|
|
def wrapped(*args, **kwargs):
|
|
calls.append(True)
|
|
return original_forward(*args, **kwargs)
|
|
|
|
module.forward = wrapped
|
|
originals = {name: (tensor.data_ptr(), tensor.stride()) for name, tensor in vae.state_dict().items()}
|
|
report = install_wan_vae_fastpath(vae, level=level)
|
|
assert not report.installed and target in report.reason and "custom forward" in report.reason
|
|
assert not is_installed(vae) and not hasattr(vae, REPORT_ATTR)
|
|
assert all(fastpath_forwards.CFG_ATTR not in child.__dict__ for child in vae.modules())
|
|
assert {name: (tensor.data_ptr(), tensor.stride()) for name, tensor in vae.state_dict().items()} == originals
|
|
uninstall_wan_vae_fastpath(vae)
|
|
assert module.forward is wrapped
|
|
vae.decode(torch.randn(1, 4, 2, 6, 8))
|
|
assert calls
|
|
|
|
|
|
@torch.no_grad()
|
|
@pytest.mark.parametrize("pre_hook", [False, True])
|
|
def test_installer_declines_convolution_hooks_it_would_bypass(pre_hook: bool) -> None:
|
|
_, vae = _build_pair(TINY_RESIDUAL, torch.float32)
|
|
conv = vae.decoder.conv_in
|
|
calls = []
|
|
|
|
def hook(*args):
|
|
calls.append(True)
|
|
|
|
handle = conv.register_forward_pre_hook(hook) if pre_hook else conv.register_forward_hook(hook)
|
|
try:
|
|
report = install_wan_vae_fastpath(vae, level="channels_last")
|
|
assert not report.installed and "decoder.conv_in" in report.reason and "forward hooks" in report.reason
|
|
vae.decode(torch.randn(1, 4, 2, 6, 8))
|
|
assert calls
|
|
finally:
|
|
handle.remove()
|
|
|
|
|
|
@torch.no_grad()
|
|
def test_installer_preserves_wrappers_and_hooks_on_modules_it_still_calls() -> None:
|
|
_, vae = _build_pair(TINY_RESIDUAL, torch.float32)
|
|
original_forward = vae.decoder.mid_block.forward
|
|
wrapper_calls = []
|
|
hook_calls = []
|
|
|
|
def wrapped(*args, **kwargs):
|
|
wrapper_calls.append(True)
|
|
return original_forward(*args, **kwargs)
|
|
|
|
def hook(*args):
|
|
hook_calls.append(True)
|
|
|
|
vae.decoder.mid_block.forward = wrapped
|
|
handle = vae.decoder.register_forward_hook(hook)
|
|
try:
|
|
latents = torch.randn(1, 4, 2, 6, 8)
|
|
expected = vae.decode(latents).sample
|
|
wrapper_calls.clear()
|
|
hook_calls.clear()
|
|
assert install_wan_vae_fastpath(vae).installed
|
|
actual = vae.decode(latents).sample
|
|
assert torch.equal(actual, expected)
|
|
assert len(wrapper_calls) == len(hook_calls) == 2
|
|
uninstall_wan_vae_fastpath(vae)
|
|
assert vae.decoder.mid_block.forward is wrapped
|
|
finally:
|
|
handle.remove()
|
|
|
|
|
|
def test_installer_refuses_unsupported_targets() -> None:
|
|
_, vae = _build_pair(TINY_RESIDUAL, torch.float32)
|
|
|
|
assert install_wan_vae_fastpath(vae, level="off").installed is False
|
|
assert not is_installed(vae)
|
|
|
|
vae._vllm_omni_wan_spatial_shard_installed = True
|
|
report = install_wan_vae_fastpath(vae)
|
|
assert not report.installed and "spatial-shard" in report.reason
|
|
del vae._vllm_omni_wan_spatial_shard_installed
|
|
|
|
vae.distributed_executor = SimpleNamespace(parallel_mode="spatial_shard_height")
|
|
report = install_wan_vae_fastpath(vae)
|
|
assert not report.installed and "spatial_shard_height" in report.reason
|
|
del vae.distributed_executor
|
|
|
|
report = install_wan_vae_fastpath(nn.Linear(2, 2))
|
|
assert not report.installed and "AutoencoderKLWan" in report.reason
|
|
|
|
with pytest.raises(ValueError, match="vae_fast_path"):
|
|
install_wan_vae_fastpath(vae, level="bogus")
|
|
|
|
|
|
@pytest.mark.parametrize(("batch", "frames"), [(1, 1), (1, 2), (2, 1)])
|
|
def test_resample_views_keep_channels_last_recognizable(batch: int, frames: int) -> None:
|
|
"""Regression: ``reshape`` gives a size-1 batch dim a stride the layout heuristic rejects."""
|
|
import torch.nn.functional as F
|
|
|
|
x = torch.randn(batch, 8, frames, 6, 10).contiguous(memory_format=torch.channels_last_3d)
|
|
merged = fastpath_forwards._merge_batch_and_frames(x)
|
|
reference = x.permute(0, 2, 1, 3, 4).reshape(batch * frames, 8, 6, 10)
|
|
assert merged.data_ptr() == x.data_ptr()
|
|
assert torch.equal(merged, reference)
|
|
upsampled = F.interpolate(merged, scale_factor=(2.0, 2.0), mode="nearest-exact")
|
|
assert upsampled.stride(1) == 1, "nearest upsample must keep channels_last for the following Conv2d"
|
|
assert torch.equal(upsampled, F.interpolate(reference.contiguous(), scale_factor=(2.0, 2.0), mode="nearest-exact"))
|
|
|
|
split = fastpath_forwards._split_batch_and_frames(upsampled, batch, frames)
|
|
assert split.shape == (batch, 8, frames, 12, 20)
|
|
assert split.data_ptr() == upsampled.data_ptr()
|
|
assert split.is_contiguous(memory_format=torch.channels_last_3d)
|
|
assert torch.equal(split, upsampled.view(batch, frames, 8, 12, 20).permute(0, 2, 1, 3, 4))
|
|
|
|
plain = torch.randn(batch, 8, frames, 6, 10)
|
|
assert torch.equal(
|
|
fastpath_forwards._merge_batch_and_frames(plain), plain.permute(0, 2, 1, 3, 4).reshape(-1, 8, 6, 10)
|
|
)
|
|
|
|
|
|
@torch.no_grad()
|
|
def test_pending_conv_bias_is_added_exactly_when_no_kernel_consumes_it() -> None:
|
|
"""Without the channels-last kernel the un-added conv bias is applied with ATen's rounding."""
|
|
from diffusers.models.autoencoders.autoencoder_kl_wan import WanRMS_norm
|
|
|
|
torch.manual_seed(0)
|
|
for dtype in (torch.float32, torch.bfloat16):
|
|
norm = WanRMS_norm(8, images=False).to(dtype)
|
|
act = nn.SiLU()
|
|
x = torch.randn(1, 8, 2, 4, 4).to(dtype)
|
|
bias = torch.randn(8).to(dtype)
|
|
biased = x.clone()
|
|
biased.add_(bias.view(1, -1, 1, 1, 1))
|
|
expected = act(norm(biased))
|
|
assert torch.equal(fastpath_forwards._norm_act(norm, act, x, pending_bias=bias), expected)
|
|
setattr(norm, fastpath_forwards.CFG_ATTR, fastpath_forwards.FastPathConfig(channels_last=True))
|
|
assert torch.equal(fastpath_forwards._norm_act(norm, act, x, pending_bias=bias), expected)
|
|
out = fastpath_forwards.rms_norm_fastpath(norm, x, bias=bias)
|
|
assert out is not None and torch.equal(out, norm(biased))
|
|
|
|
|
|
@torch.no_grad()
|
|
def test_resample_return_bias_is_none_without_kernels() -> None:
|
|
"""On CPU the Conv2d keeps its bias, so the up block adds nothing twice."""
|
|
from diffusers.models.autoencoders.autoencoder_kl_wan import WanResample, WanResidualUpBlock
|
|
|
|
torch.manual_seed(0)
|
|
resample = WanResample(8, mode="upsample2d").eval()
|
|
assert fastpath_forwards._is_upsample_conv_pair(resample.resample)
|
|
assert not fastpath_forwards._is_upsample_conv_pair(WanResample(8, mode="downsample2d").resample)
|
|
x = torch.randn(1, 8, 2, 6, 10)
|
|
out, bias = fastpath_forwards.resample_forward(resample, x, [None], [0], return_bias=True)
|
|
assert bias is None
|
|
assert torch.equal(out, WanResample.forward(resample, x, feat_cache=[None], feat_idx=[0]))
|
|
|
|
# First chunk of a temporal up block: one input frame, the time conv is skipped ("Rep").
|
|
block = WanResidualUpBlock(8, 8, num_res_blocks=1, temperal_upsample=True, up_flag=True).eval()
|
|
for module in block.modules():
|
|
setattr(module, fastpath_forwards.CFG_ATTR, fastpath_forwards.FastPathConfig())
|
|
x = torch.randn(1, 8, 1, 6, 10)
|
|
cache_len = 6
|
|
expected = WanResidualUpBlock.forward(block, x, feat_cache=[None] * cache_len, feat_idx=[0], first_chunk=True)
|
|
actual = fastpath_forwards.residual_up_block_forward(
|
|
block, x, feat_cache=[None] * cache_len, feat_idx=[0], first_chunk=True
|
|
)
|
|
assert torch.equal(actual, expected)
|
|
|
|
|
|
def test_upsample_forward_only_fuses_nearest_2x() -> None:
|
|
from diffusers.models.autoencoders.autoencoder_kl_wan import WanUpsample
|
|
|
|
assert fastpath_forwards._is_nearest_2x(WanUpsample(scale_factor=(2.0, 2.0), mode="nearest-exact"))
|
|
assert fastpath_forwards._is_nearest_2x(WanUpsample(scale_factor=2, mode="nearest"))
|
|
assert not fastpath_forwards._is_nearest_2x(WanUpsample(scale_factor=(2.0, 3.0), mode="nearest-exact"))
|
|
assert not fastpath_forwards._is_nearest_2x(WanUpsample(scale_factor=(2.0, 2.0), mode="bilinear"))
|
|
assert not fastpath_forwards._is_nearest_2x(WanUpsample(size=(12, 20), mode="nearest-exact"))
|
|
|
|
module = WanUpsample(scale_factor=(2.0, 2.0), mode="nearest-exact")
|
|
for dtype in (torch.float32, torch.bfloat16):
|
|
x = torch.randn(3, 8, 6, 10).to(dtype)
|
|
for layout in (torch.contiguous_format, torch.channels_last):
|
|
x = x.contiguous(memory_format=layout)
|
|
expected = WanUpsample.forward(module, x)
|
|
actual = fastpath_forwards.upsample_forward(module, x)
|
|
assert actual.stride() == expected.stride()
|
|
assert torch.equal(actual, expected)
|
|
|
|
|
|
def test_rms_norm_vae_substitute_is_not_matched() -> None:
|
|
from vllm_omni.diffusion.layers.norm import RMSNormVAE
|
|
|
|
assert not fastpath_forwards.is_diffusers_rms_norm(RMSNormVAE(8, images=False))
|
|
_, vae = _build_pair(TINY_RESIDUAL, torch.float32)
|
|
norm = vae.decoder.norm_out
|
|
assert fastpath_forwards.is_diffusers_rms_norm(norm)
|
|
|
|
|
|
@torch.no_grad()
|
|
def test_rms_norm_fastpath_declines_tensor_bias_and_dtype_mismatch() -> None:
|
|
from diffusers.models.autoencoders.autoencoder_kl_wan import WanRMS_norm
|
|
|
|
norm = WanRMS_norm(8, images=False, bias=True)
|
|
assert fastpath_forwards.rms_norm_fastpath(norm, torch.randn(1, 8, 1, 4, 4)) is None
|
|
|
|
norm = WanRMS_norm(8, images=False)
|
|
assert fastpath_forwards.rms_norm_fastpath(norm, torch.randn(1, 8, 1, 4, 4).to(torch.bfloat16)) is None
|
|
|
|
x = torch.randn(1, 8, 2, 4, 4)
|
|
out = fastpath_forwards.rms_norm_fastpath(norm, x)
|
|
assert out is not None and torch.equal(out, norm(x))
|
|
|
|
|
|
def test_rms_norm_forward_preserves_autograd() -> None:
|
|
from diffusers.models.autoencoders.autoencoder_kl_wan import WanRMS_norm
|
|
|
|
torch.manual_seed(0)
|
|
reference = WanRMS_norm(8, images=False)
|
|
candidate = WanRMS_norm(8, images=False)
|
|
candidate.load_state_dict(reference.state_dict())
|
|
reference_input = torch.randn(1, 8, 1, 4, 4, requires_grad=True)
|
|
candidate_input = reference_input.detach().clone().requires_grad_()
|
|
|
|
expected = reference(reference_input)
|
|
assert fastpath_forwards.rms_norm_fastpath(candidate, candidate_input) is None
|
|
actual = fastpath_forwards.rms_norm_forward(candidate, candidate_input)
|
|
torch.testing.assert_close(actual, expected)
|
|
|
|
expected.square().mean().backward()
|
|
actual.square().mean().backward()
|
|
torch.testing.assert_close(candidate_input.grad, reference_input.grad)
|
|
torch.testing.assert_close(candidate.gamma.grad, reference.gamma.grad)
|
|
|
|
|
|
def test_omni_diffusion_config_validates_vae_fast_path() -> None:
|
|
assert OmniDiffusionConfig(model="x").vae_fast_path == "lossless"
|
|
assert OmniDiffusionConfig(model="x", vae_fast_path="channels_last").vae_fast_path == "channels_last"
|
|
with pytest.raises(ValueError, match="vae_fast_path"):
|
|
OmniDiffusionConfig(model="x", vae_fast_path="fast")
|
|
|
|
|
|
class _StubPipeline(nn.Module):
|
|
def __init__(self, vae: nn.Module) -> None:
|
|
super().__init__()
|
|
self.vae = vae
|
|
|
|
|
|
@pytest.mark.parametrize(("level", "expected"), [("lossless", True), ("channels_last", True), ("off", False)])
|
|
def test_registry_hook_installs_on_cuda_platform(mocker, level: str, expected: bool) -> None:
|
|
_, vae = _build_pair(TINY_RESIDUAL, torch.float32)
|
|
platform = mocker.Mock()
|
|
platform.is_cuda.return_value = True
|
|
mocker.patch.object(registry_module, "current_omni_platform", platform)
|
|
|
|
registry_module._apply_wan_vae_fastpath_if_enabled(_StubPipeline(vae), SimpleNamespace(vae_fast_path=level))
|
|
assert is_installed(vae) is expected
|
|
if expected:
|
|
assert getattr(vae, REPORT_ATTR).level == level
|
|
|
|
|
|
def test_registry_hook_skips_non_cuda_platform_and_non_wan_vaes(mocker) -> None:
|
|
_, vae = _build_pair(TINY_RESIDUAL, torch.float32)
|
|
platform = mocker.Mock()
|
|
platform.is_cuda.return_value = False
|
|
mocker.patch.object(registry_module, "current_omni_platform", platform)
|
|
registry_module._apply_wan_vae_fastpath_if_enabled(_StubPipeline(vae), SimpleNamespace(vae_fast_path="lossless"))
|
|
assert not is_installed(vae)
|
|
|
|
platform.is_cuda.return_value = True
|
|
other = nn.Linear(2, 2)
|
|
registry_module._apply_wan_vae_fastpath_if_enabled(_StubPipeline(other), SimpleNamespace(vae_fast_path="lossless"))
|
|
assert not hasattr(other, REPORT_ATTR)
|