Files
vllm-omni/tests/model_executor/models/indextts2/test_wavenet_ragged.py
T
2026-08-11 23:00:35 +00:00

197 lines
6.5 KiB
Python

# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
from types import SimpleNamespace
import pytest
import torch
from vllm_omni.model_executor.models.indextts2.indextts2_s2mel_decoder import (
IndexTTS2S2MelDecoder,
)
from vllm_omni.model_executor.models.indextts2.s2mel.modules.wavenet import WN
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
def _make_wavenet() -> WN:
torch.manual_seed(0)
return WN(
hidden_channels=2,
kernel_size=5,
dilation_rate=1,
n_layers=8,
gin_channels=2,
p_dropout=0.0,
causal=False,
).eval()
def _sequence_mask(lengths: torch.Tensor, max_length: int) -> torch.Tensor:
positions = torch.arange(max_length, device=lengths.device)
return (positions.unsqueeze(0) < lengths.unsqueeze(1)).unsqueeze(1)
def _singleton_outputs(
wavenet: WN,
inputs: torch.Tensor,
lengths: list[int],
conditioning: torch.Tensor,
) -> list[torch.Tensor]:
# This is the independent oracle: every request ends at the physical tensor
# boundary and therefore uses SConv1d's original reflect-padding behavior.
wavenet._assume_full_mask = True
return [
wavenet(
inputs[row : row + 1, :, :length],
torch.ones(1, 1, length, dtype=torch.bool),
g=conditioning[row : row + 1],
)
for row, length in enumerate(lengths)
]
def test_ragged_wavenet_matches_original_singleton_reflect_boundaries():
wavenet = _make_wavenet()
lengths = [6, 5, 4, 3]
inputs = torch.randn(len(lengths), 2, max(lengths))
conditioning = torch.randn(len(lengths), 2, 1)
expected = _singleton_outputs(wavenet, inputs, lengths, conditioning)
mask = _sequence_mask(torch.tensor(lengths), inputs.shape[-1])
wavenet._assume_full_mask = False
# Keep deliberately non-zero invalid tails. The first WaveNet layer sees
# transformer output before the recurrent mask has cleared those values.
actual = wavenet(inputs, mask, g=conditioning)
for row, (length, singleton) in enumerate(zip(lengths, expected)):
torch.testing.assert_close(
actual[row : row + 1, :, :length],
singleton,
rtol=1e-5,
atol=1e-6,
)
def test_wavenet_exposes_minimum_safe_ragged_length():
# kernel=5, dilation=1 has two right-reflect ghost positions, so a
# logical row must contain at least three frames before it can use the
# vectorized ragged path. Shorter rows stay on original singleton SConv1d.
assert _make_wavenet().ragged_min_length == 3
def test_cfm_grouping_isolates_rows_too_short_for_logical_reflect():
groups = IndexTTS2S2MelDecoder._cfm_group_indices(
sorted_indices=[0, 1, 2, 3, 4],
total_lengths=[0, 1, 2, 3, 6],
max_group_size=2,
min_ragged_length=3,
)
assert groups == [[0], [1], [2], [3, 4]]
def test_ragged_wavenet_preserves_cfg_cond_uncond_row_order():
wavenet = _make_wavenet()
base_lengths = [3, 6]
conditional = torch.randn(2, 2, 6)
unconditional = torch.randn(2, 2, 6)
cfg_inputs = torch.cat([conditional, unconditional], dim=0)
cfg_lengths = base_lengths + base_lengths
cfg_conditioning = torch.randn(4, 2, 1)
expected = _singleton_outputs(
wavenet,
cfg_inputs,
cfg_lengths,
cfg_conditioning,
)
mask = _sequence_mask(torch.tensor(cfg_lengths), cfg_inputs.shape[-1])
wavenet._assume_full_mask = False
actual = wavenet(cfg_inputs, mask, g=cfg_conditioning)
for row, (length, singleton) in enumerate(zip(cfg_lengths, expected)):
torch.testing.assert_close(
actual[row : row + 1, :, :length],
singleton,
rtol=1e-5,
atol=1e-6,
)
def test_full_mask_wavenet_path_is_bit_exact_with_original_path():
wavenet = _make_wavenet()
inputs = torch.randn(3, 2, 6)
mask = torch.ones(3, 1, 6, dtype=torch.bool)
conditioning = torch.randn(3, 2, 1)
if hasattr(wavenet, "_assume_full_mask"):
del wavenet._assume_full_mask
expected = wavenet(inputs, mask, g=conditioning)
wavenet._assume_full_mask = True
actual = wavenet(inputs, mask, g=conditioning)
assert torch.equal(actual, expected)
@pytest.mark.skipif(not hasattr(torch, "compile"), reason="torch.compile unavailable")
def test_ragged_wavenet_is_one_dynamic_fullgraph():
wavenet = _make_wavenet()
lengths = [3, 6]
inputs = torch.randn(2, 2, 6)
conditioning = torch.randn(2, 2, 1)
mask = _sequence_mask(torch.tensor(lengths), inputs.shape[-1])
expected = _singleton_outputs(wavenet, inputs, lengths, conditioning)
wavenet._assume_full_mask = False
compiled = torch.compile(
wavenet,
backend="eager",
fullgraph=True,
dynamic=True,
)
actual = compiled(inputs, mask, g=conditioning)
for row, (length, singleton) in enumerate(zip(lengths, expected)):
torch.testing.assert_close(
actual[row : row + 1, :, :length],
singleton,
rtol=1e-5,
atol=1e-6,
)
def test_decoder_full_mask_switch_also_controls_wavenet_boundary_path():
decoder = object.__new__(IndexTTS2S2MelDecoder)
attention_layers = [SimpleNamespace(attention=SimpleNamespace()) for _ in range(2)]
estimator = SimpleNamespace(
transformer=SimpleNamespace(layers=attention_layers),
wavenet=SimpleNamespace(),
)
decoder._set_dit_full_mask_fast_path(estimator, enabled=False)
assert all(layer.attention._assume_full_mask is False for layer in attention_layers)
assert estimator.wavenet._assume_full_mask is False
decoder._set_dit_full_mask_fast_path(estimator, enabled=True)
assert all(layer.attention._assume_full_mask is True for layer in attention_layers)
assert estimator.wavenet._assume_full_mask is True
def test_decoder_full_mask_switch_unwraps_compiled_estimator():
decoder = object.__new__(IndexTTS2S2MelDecoder)
attention_layers = [SimpleNamespace(attention=SimpleNamespace()) for _ in range(2)]
eager_estimator = SimpleNamespace(
transformer=SimpleNamespace(layers=attention_layers),
wavenet=SimpleNamespace(),
)
compiled_estimator = SimpleNamespace(_orig_mod=eager_estimator)
decoder._set_dit_full_mask_fast_path(compiled_estimator, enabled=False)
assert all(layer.attention._assume_full_mask is False for layer in attention_layers)
assert eager_estimator.wavenet._assume_full_mask is False