Signed-off-by: Sysy <1370724210@qq.com> Co-authored-by: Yueqian Lin <70319226+linyueqian@users.noreply.github.com>
197 lines
6.5 KiB
Python
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
|