feat(diffusion): add request-gated VAE fast paths
Signed-off-by: Hongsheng Liu <liuhongsheng4@huawei.com>
This commit is contained in:
1 parent
c27623c29c
commit
094abd70c1
22 files changed
+1591
-4
No files matched your search
@@ -113,6 +113,14 @@ class TestCreateDefaultDiffusion:
|
||||
stages = StageConfigFactory.create_default_diffusion({"model": "x"})
|
||||
assert stages[0]["engine_args"]["cache_backend"] == "none"
|
||||
|
||||
def test_vae_decode_precision_roundtrip(self):
|
||||
od = _roundtrip_diffusion_config(model="x", vae_decode_precision="bf16")
|
||||
assert od.vae_decode_precision == "bf16"
|
||||
|
||||
def test_invalid_vae_decode_precision_is_rejected(self):
|
||||
with pytest.raises(ValueError, match="vae_decode_precision"):
|
||||
OmniDiffusionConfig(model="x", vae_decode_precision="int8")
|
||||
|
||||
def test_single_gpu_default_devices(self):
|
||||
stages = StageConfigFactory.create_default_diffusion({"model": "x"})
|
||||
assert stages[0]["runtime"]["devices"] == "0"
|
||||
|
||||
@@ -512,6 +512,35 @@ def test_execute_model_accepts_bare_diffusion_output_from_single_request_pipelin
|
||||
assert runner.pipeline.last_req.num_reqs == 1
|
||||
|
||||
|
||||
@pytest.mark.core_model
|
||||
@pytest.mark.cpu
|
||||
@pytest.mark.parametrize(
|
||||
("quality", "expected_enabled"),
|
||||
[(None, False), ("lossless", False), ("high", True)],
|
||||
)
|
||||
def test_execute_model_scopes_vae_fast_path_to_request_quality(monkeypatch, quality, expected_enabled):
|
||||
runner = _make_runner(cache_backend=None, cache_backend_name="none")
|
||||
req = _make_request()
|
||||
req.sampling_params.quality = quality
|
||||
gate_calls = []
|
||||
|
||||
@contextmanager
|
||||
def _record_gate(pipeline, enabled):
|
||||
gate_calls.append((pipeline, enabled, "enter"))
|
||||
yield
|
||||
gate_calls.append((pipeline, enabled, "exit"))
|
||||
|
||||
monkeypatch.setattr(model_runner_module, "set_forward_context", _noop_forward_context)
|
||||
monkeypatch.setattr(model_runner_module, "use_pipeline_vae_fast_path", _record_gate)
|
||||
|
||||
DiffusionModelRunner.execute_model(runner, req)
|
||||
|
||||
assert gate_calls == [
|
||||
(runner.pipeline, expected_enabled, "enter"),
|
||||
(runner.pipeline, expected_enabled, "exit"),
|
||||
]
|
||||
|
||||
|
||||
class _BatchPipeline:
|
||||
"""Pipeline returning a configurable list of outputs from forward()."""
|
||||
|
||||
@@ -579,6 +608,18 @@ def test_execute_model_batch_rejects_output_count_mismatch(monkeypatch):
|
||||
DiffusionModelRunner.execute_model_batch(runner, sched, runner.od_config)
|
||||
|
||||
|
||||
@pytest.mark.core_model
|
||||
@pytest.mark.cpu
|
||||
def test_execute_model_batch_rejects_mixed_vae_quality() -> None:
|
||||
runner = _make_batch_runner(_BatchPipeline(outputs=[DiffusionOutput(output="a"), DiffusionOutput(output="b")]))
|
||||
sched = _make_scheduler_output(num_reqs=2)
|
||||
sched.scheduled_new_reqs[0].req.sampling_params.quality = "lossless"
|
||||
sched.scheduled_new_reqs[1].req.sampling_params.quality = "high"
|
||||
|
||||
with pytest.raises(RuntimeError, match="cannot mix request quality levels"):
|
||||
DiffusionModelRunner.execute_model_batch(runner, sched, runner.od_config)
|
||||
|
||||
|
||||
@pytest.mark.core_model
|
||||
@pytest.mark.cpu
|
||||
def test_execute_model_batch_routes_one_output_per_request(monkeypatch):
|
||||
@@ -728,6 +769,19 @@ def test_execute_model_runs_forward_after_kv_receive(monkeypatch):
|
||||
@pytest.mark.core_model
|
||||
@pytest.mark.cpu
|
||||
def test_load_model_clears_cache_backend_for_unsupported_pipeline(monkeypatch):
|
||||
memory_pool_active = False
|
||||
optimization_pool_states = []
|
||||
|
||||
@contextmanager
|
||||
def _memory_pool_context(tag):
|
||||
nonlocal memory_pool_active
|
||||
assert tag == "weights"
|
||||
memory_pool_active = True
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
memory_pool_active = False
|
||||
|
||||
class _DummyLoader:
|
||||
def __init__(self, load_config, od_config=None):
|
||||
del load_config, od_config
|
||||
@@ -775,16 +829,22 @@ def test_load_model_clears_cache_backend_for_unsupported_pipeline(monkeypatch):
|
||||
monkeypatch.setattr(model_runner_module, "LoadConfig", lambda: object())
|
||||
monkeypatch.setattr(model_runner_module, "DiffusersPipelineLoader", _DummyLoader)
|
||||
monkeypatch.setattr(model_runner_module, "DeviceMemoryProfiler", _DummyMemoryProfiler)
|
||||
monkeypatch.setattr(
|
||||
model_runner_module,
|
||||
"optimize_pipeline_vaes",
|
||||
lambda pipeline, od_config: optimization_pool_states.append(memory_pool_active),
|
||||
)
|
||||
monkeypatch.setattr(model_runner_module, "get_offload_backend", lambda od_config, device: None)
|
||||
monkeypatch.setattr(
|
||||
model_runner_module, "get_cache_backend", lambda cache_backend, cache_config: dummy_cache_backend
|
||||
)
|
||||
|
||||
DiffusionModelRunner.load_model(runner)
|
||||
DiffusionModelRunner.load_model(runner, memory_pool_context_fn=_memory_pool_context)
|
||||
|
||||
assert runner.cache_backend is None
|
||||
assert runner.od_config.cache_backend is None
|
||||
assert dummy_cache_backend.enabled is False
|
||||
assert optimization_pool_states == [True]
|
||||
|
||||
|
||||
@pytest.mark.core_model
|
||||
|
||||
@@ -102,6 +102,7 @@ class TestDiffusionWorkerSleep:
|
||||
|
||||
def test_sleep_level_1(self, mocker: MockerFixture, mock_gpu_worker):
|
||||
"""Test sleep mode level 1 (offload weights only)."""
|
||||
clear_vae_caches = mocker.patch("vllm_omni.diffusion.vae_optimizations.clear_pipeline_vae_fast_path_caches")
|
||||
mock_allocator_class = patch_cumem_allocator(mocker)
|
||||
mock_platform = mocker.patch("vllm_omni.diffusion.worker.diffusion_worker.current_omni_platform")
|
||||
mock_platform.get_free_memory.side_effect = [10 * 1024**3, 12 * 1024**3]
|
||||
@@ -128,6 +129,7 @@ class TestDiffusionWorkerSleep:
|
||||
|
||||
# Verify sleep was called with correct tags
|
||||
mock_allocator.sleep.assert_called_once_with(offload_tags=("weights",))
|
||||
clear_vae_caches.assert_called_once_with(mock_gpu_worker.model_runner.pipeline)
|
||||
assert bool(result) is True
|
||||
# Verify buffers were NOT saved (level 1 doesn't save buffers)
|
||||
assert len(mock_gpu_worker._sleep_saved_buffers) == 0
|
||||
|
||||
@@ -0,0 +1,300 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""Correctness contracts for request-gated VAE decoder fast paths."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
from torch import nn
|
||||
|
||||
from tests.helpers.mark import hardware_test
|
||||
from vllm_omni.diffusion.vae_optimizations.gate import (
|
||||
VaeFastPathGate,
|
||||
register_vae_fast_path_gate,
|
||||
use_pipeline_vae_fast_path,
|
||||
use_vae_fast_path,
|
||||
)
|
||||
|
||||
pytestmark = [pytest.mark.diffusion]
|
||||
|
||||
|
||||
class _PipelineWithVae(nn.Module):
|
||||
def __init__(self, vae: nn.Module) -> None:
|
||||
super().__init__()
|
||||
self.vae = vae
|
||||
|
||||
|
||||
class _DistributedExecutorConfig:
|
||||
def __init__(self, parallel_mode: str) -> None:
|
||||
self.parallel_mode = parallel_mode
|
||||
|
||||
|
||||
@pytest.mark.core_model
|
||||
@pytest.mark.cpu
|
||||
def test_vae_fast_path_gate_restores_nested_state() -> None:
|
||||
vae = nn.Identity()
|
||||
gate = VaeFastPathGate()
|
||||
register_vae_fast_path_gate(vae, gate)
|
||||
|
||||
with use_vae_fast_path(vae, True):
|
||||
assert gate.enabled
|
||||
with use_vae_fast_path(vae, False):
|
||||
assert not gate.enabled
|
||||
assert gate.enabled
|
||||
|
||||
assert not gate.enabled
|
||||
|
||||
|
||||
@pytest.mark.core_model
|
||||
@pytest.mark.cpu
|
||||
def test_pipeline_vae_fast_path_controls_discovered_vae() -> None:
|
||||
vae = nn.Identity()
|
||||
pipeline = _PipelineWithVae(vae)
|
||||
gate = VaeFastPathGate()
|
||||
register_vae_fast_path_gate(vae, gate)
|
||||
|
||||
with use_pipeline_vae_fast_path(pipeline, True):
|
||||
assert gate.enabled
|
||||
|
||||
assert not gate.enabled
|
||||
|
||||
|
||||
@pytest.mark.core_model
|
||||
@pytest.mark.cpu
|
||||
def test_image_group_norm_fast_path_uses_compile_safe_fallback(monkeypatch) -> None:
|
||||
from vllm_omni.diffusion.vae_optimizations import image
|
||||
|
||||
gate = VaeFastPathGate()
|
||||
gate.enabled = True
|
||||
norm = image.FusedGroupNormSiLU(nn.GroupNorm(2, 4), gate)
|
||||
inputs = torch.randn(1, 4, 3, 3)
|
||||
|
||||
monkeypatch.setattr(torch.compiler, "is_compiling", lambda: True)
|
||||
monkeypatch.setattr(
|
||||
image,
|
||||
"group_norm_silu_4d",
|
||||
lambda *args, **kwargs: pytest.fail("eager Triton kernel entered during compilation"),
|
||||
)
|
||||
|
||||
actual = norm(inputs)
|
||||
expected = F.silu(F.group_norm(inputs, 2, norm.weight, norm.bias, norm.eps))
|
||||
torch.testing.assert_close(actual, expected)
|
||||
|
||||
|
||||
def _small_image_vae() -> nn.Module:
|
||||
from diffusers.models.autoencoders import AutoencoderKL
|
||||
|
||||
return AutoencoderKL(
|
||||
in_channels=3,
|
||||
out_channels=3,
|
||||
down_block_types=("DownEncoderBlock2D", "DownEncoderBlock2D"),
|
||||
up_block_types=("UpDecoderBlock2D", "UpDecoderBlock2D"),
|
||||
block_out_channels=(32, 32),
|
||||
layers_per_block=1,
|
||||
latent_channels=4,
|
||||
norm_num_groups=8,
|
||||
sample_size=16,
|
||||
)
|
||||
|
||||
|
||||
def _small_wan_vae(vae_cls: type[nn.Module] | None = None) -> nn.Module:
|
||||
if vae_cls is None:
|
||||
from diffusers.models.autoencoders import AutoencoderKLWan
|
||||
|
||||
vae_cls = AutoencoderKLWan
|
||||
|
||||
return vae_cls(
|
||||
base_dim=8,
|
||||
decoder_base_dim=8,
|
||||
z_dim=4,
|
||||
dim_mult=[1, 2],
|
||||
num_res_blocks=1,
|
||||
attn_scales=[],
|
||||
temperal_downsample=[False],
|
||||
latents_mean=[0.0] * 4,
|
||||
latents_std=[1.0] * 4,
|
||||
scale_factor_temporal=1,
|
||||
scale_factor_spatial=2,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.core_model
|
||||
@hardware_test(res={"cuda": "L4"}, num_cards=1)
|
||||
@torch.no_grad()
|
||||
def test_image_vae_install_preserves_lossless_path_and_state_dict() -> None:
|
||||
from vllm_omni.diffusion.vae_optimizations import clear_pipeline_vae_fast_path_caches
|
||||
from vllm_omni.diffusion.vae_optimizations.image import (
|
||||
FusedGroupNormSiLU,
|
||||
FusedUpsample2xConv2d,
|
||||
maybe_optimize_image_vae,
|
||||
)
|
||||
|
||||
torch.manual_seed(0)
|
||||
vae = _small_image_vae().to(device="cuda", dtype=torch.bfloat16).eval()
|
||||
latents = torch.randn(1, 4, 8, 8, device="cuda", dtype=torch.bfloat16)
|
||||
parameter_names = {name for name, _ in vae.named_parameters()}
|
||||
state_dict = {name: value.clone() for name, value in vae.state_dict().items()}
|
||||
reference = vae.decode(latents).sample
|
||||
|
||||
maybe_optimize_image_vae(vae)
|
||||
maybe_optimize_image_vae(vae)
|
||||
|
||||
assert {name for name, _ in vae.named_parameters()} == parameter_names
|
||||
vae.load_state_dict(state_dict, strict=True)
|
||||
assert any(isinstance(module, FusedGroupNormSiLU) for module in vae.modules())
|
||||
assert any(isinstance(module, FusedUpsample2xConv2d) for module in vae.modules())
|
||||
assert torch.equal(vae.decode(latents).sample, reference)
|
||||
|
||||
with use_vae_fast_path(vae, True):
|
||||
fast = vae.decode(latents).sample
|
||||
|
||||
torch.testing.assert_close(fast.float(), reference.float(), atol=0.1, rtol=0)
|
||||
assert any(
|
||||
buffer is not None
|
||||
for module in vae.modules()
|
||||
for name, buffer in module._buffers.items()
|
||||
if name in {"_fused_weight", "_vllm_folded_value_weight", "_vllm_folded_value_bias"}
|
||||
)
|
||||
clear_pipeline_vae_fast_path_caches(_PipelineWithVae(vae))
|
||||
assert all(
|
||||
buffer is None
|
||||
for module in vae.modules()
|
||||
for name, buffer in module._buffers.items()
|
||||
if name in {"_fused_weight", "_vllm_folded_value_weight", "_vllm_folded_value_bias"}
|
||||
)
|
||||
with use_vae_fast_path(vae, True):
|
||||
torch.testing.assert_close(vae.decode(latents).sample.float(), reference.float(), atol=0.1, rtol=0)
|
||||
assert torch.equal(vae.decode(latents).sample, reference)
|
||||
assert vae.decoder.conv_in.weight.is_contiguous()
|
||||
|
||||
|
||||
@pytest.mark.core_model
|
||||
@hardware_test(res={"cuda": "L4"}, num_cards=1)
|
||||
@torch.no_grad()
|
||||
@pytest.mark.parametrize("dtype", [torch.float32, torch.bfloat16])
|
||||
def test_channels_last_group_norm_silu_matches_reference(dtype: torch.dtype) -> None:
|
||||
from vllm_omni.diffusion.vae_optimizations.triton_group_norm_silu import group_norm_silu_4d
|
||||
|
||||
torch.manual_seed(0)
|
||||
x = torch.randn(1, 64, 16, 16, device="cuda", dtype=dtype).contiguous(memory_format=torch.channels_last)
|
||||
weight = torch.randn(64, device="cuda", dtype=dtype)
|
||||
bias = torch.randn(64, device="cuda", dtype=dtype)
|
||||
|
||||
actual = group_norm_silu_4d(x, weight, bias, num_groups=8, eps=1e-5)
|
||||
expected = F.silu(F.group_norm(x, 8, weight, bias))
|
||||
|
||||
assert actual is not None
|
||||
atol, rtol = (1e-5, 1e-5) if dtype == torch.float32 else (7e-2, 2e-2)
|
||||
torch.testing.assert_close(actual, expected, atol=atol, rtol=rtol)
|
||||
|
||||
|
||||
@pytest.mark.core_model
|
||||
@hardware_test(res={"cuda": "L4"}, num_cards=1)
|
||||
@torch.no_grad()
|
||||
@pytest.mark.parametrize(
|
||||
"activation_dtype,affine_dtype,atol,rtol",
|
||||
[
|
||||
(torch.float32, torch.float32, 1e-5, 1e-5),
|
||||
(torch.bfloat16, torch.float32, 1.5e-1, 3e-2),
|
||||
],
|
||||
)
|
||||
def test_wan_rmsnorm_silu_matches_reference(
|
||||
activation_dtype: torch.dtype,
|
||||
affine_dtype: torch.dtype,
|
||||
atol: float,
|
||||
rtol: float,
|
||||
) -> None:
|
||||
from vllm_omni.diffusion.vae_optimizations.triton_wan_rmsnorm_silu import wan_rmsnorm_silu
|
||||
|
||||
torch.manual_seed(0)
|
||||
x = torch.randn(1, 96, 3, 10, 14, device="cuda", dtype=activation_dtype).contiguous(
|
||||
memory_format=torch.channels_last_3d
|
||||
)
|
||||
gamma = torch.randn(96, 1, 1, 1, device="cuda", dtype=affine_dtype)
|
||||
bias = torch.randn_like(gamma)
|
||||
normalized = F.normalize(x.float() if activation_dtype != torch.float32 else x, dim=1).to(activation_dtype)
|
||||
expected = F.silu(normalized * 96**0.5 * gamma + bias)
|
||||
|
||||
actual = wan_rmsnorm_silu(x, gamma, bias)
|
||||
|
||||
assert actual is not None
|
||||
assert actual.stride() == x.stride()
|
||||
torch.testing.assert_close(actual, expected, atol=atol, rtol=rtol)
|
||||
|
||||
|
||||
@pytest.mark.core_model
|
||||
@hardware_test(res={"cuda": "L4"}, num_cards=1)
|
||||
@torch.no_grad()
|
||||
def test_wan_vae_install_preserves_lossless_path_and_state_dict() -> None:
|
||||
from vllm_omni.diffusion.vae_optimizations.wan import (
|
||||
FusedWanRMSNormSiLU,
|
||||
maybe_optimize_wan_vae,
|
||||
)
|
||||
|
||||
torch.manual_seed(0)
|
||||
vae = _small_wan_vae().cuda().eval()
|
||||
latents = torch.randn(1, 4, 1, 8, 8, device="cuda")
|
||||
parameter_names = {name for name, _ in vae.named_parameters()}
|
||||
state_dict = {name: value.clone() for name, value in vae.state_dict().items()}
|
||||
reference = vae.decode(latents).sample
|
||||
|
||||
maybe_optimize_wan_vae(vae)
|
||||
maybe_optimize_wan_vae(vae)
|
||||
|
||||
assert {name for name, _ in vae.named_parameters()} == parameter_names
|
||||
vae.load_state_dict(state_dict, strict=True)
|
||||
assert any(isinstance(module, FusedWanRMSNormSiLU) for module in vae.modules())
|
||||
assert torch.equal(vae.decode(latents).sample, reference)
|
||||
|
||||
with use_vae_fast_path(vae, True):
|
||||
fast = vae.decode(latents).sample
|
||||
|
||||
torch.testing.assert_close(fast, reference, atol=5e-4, rtol=1e-4)
|
||||
assert torch.equal(vae.decode(latents).sample, reference)
|
||||
|
||||
|
||||
@pytest.mark.core_model
|
||||
@pytest.mark.cpu
|
||||
@pytest.mark.parametrize("parallel_mode", ["auto", "spatial_shard_height", "spatial_shard_width"])
|
||||
def test_wan_fast_path_skips_spatial_shard_mode(monkeypatch: pytest.MonkeyPatch, parallel_mode: str) -> None:
|
||||
import vllm_omni.diffusion.vae_optimizations.wan as wan_optimizations
|
||||
|
||||
monkeypatch.setattr(wan_optimizations, "_HAS_TRITON", True)
|
||||
vae = _small_wan_vae()
|
||||
vae.distributed_executor = _DistributedExecutorConfig(parallel_mode)
|
||||
|
||||
wan_optimizations.maybe_optimize_wan_vae(vae)
|
||||
|
||||
assert not any(isinstance(module, wan_optimizations.FusedWanRMSNormSiLU) for module in vae.modules())
|
||||
|
||||
|
||||
@pytest.mark.core_model
|
||||
@hardware_test(res={"cuda": "L4"}, num_cards=1)
|
||||
@torch.no_grad()
|
||||
@pytest.mark.parametrize(
|
||||
("precision", "decode_dtype"),
|
||||
[("fp32", torch.float32), ("fp16", torch.float16), ("bf16", torch.bfloat16)],
|
||||
)
|
||||
def test_wan_decode_precision_keeps_encode_fp32(precision: str, decode_dtype: torch.dtype) -> None:
|
||||
from vllm_omni.diffusion.distributed.autoencoders.autoencoder_kl_wan import DistributedAutoencoderKLWan
|
||||
from vllm_omni.diffusion.vae_optimizations.precision import configure_wan_decode_precision
|
||||
|
||||
torch.manual_seed(0)
|
||||
vae = _small_wan_vae(DistributedAutoencoderKLWan).to(device="cuda", dtype=torch.bfloat16).eval()
|
||||
configure_wan_decode_precision(vae, precision)
|
||||
|
||||
assert next(vae.encoder.parameters()).dtype == torch.float32
|
||||
assert next(vae.quant_conv.parameters()).dtype == torch.float32
|
||||
assert next(vae.decoder.parameters()).dtype == decode_dtype
|
||||
assert next(vae.post_quant_conv.parameters()).dtype == decode_dtype
|
||||
|
||||
_, grid_spec = vae.tile_split(torch.randn(1, 4, 1, 8, 8, device="cuda", dtype=decode_dtype))
|
||||
assert grid_spec.output_dtype == decode_dtype
|
||||
|
||||
encoded = vae.encode(torch.randn(1, 3, 1, 16, 16, device="cuda", dtype=torch.bfloat16))
|
||||
decoded = vae.decode(torch.randn(1, 4, 1, 8, 8, device="cuda", dtype=torch.float32))
|
||||
|
||||
assert encoded.latent_dist.parameters.dtype == torch.float32
|
||||
assert decoded.sample.dtype == decode_dtype
|
||||
Reference in new issue
Block a user