diff --git a/.buildkite/amd/test-amd-nightly.yml b/.buildkite/amd/test-amd-nightly.yml index 0da365d0c..4544a4f20 100644 --- a/.buildkite/amd/test-amd-nightly.yml +++ b/.buildkite/amd/test-amd-nightly.yml @@ -64,3 +64,21 @@ steps: commands: - export VLLM_ROCM_USE_AITER=1 - pytest -s -v tests/e2e/online_serving/test_qwen3_omni_expansion.py::test_text_to_text_audio_001 -m "full_model and rocm and MI325 and cards_2" --run-level "full_model" + + - group: ":speaker: CosyVoice3 MI300 Nightly" + steps: + # Exercise all sync/async and Chinese/English cases with the platform's + # default attention backend. On ROCm this retains end-to-end AITER + # coverage without putting its cold JIT build on the READY critical path. + - label: "CosyVoice3-TTS E2E Test" + agent_pool: mi300_1 + depends_on: amd-build + mirror_hardwares: [amdproduction] + grade: NonBlocking + timeout_in_minutes: 90 + retry: + automatic: + - exit_status: 134 + limit: 1 + commands: + - pytest -s -v tests/e2e/online_serving/test_cosyvoice3_tts_expansion.py -m "slow" --run-level "core_model" diff --git a/.buildkite/amd/test-amd-ready.yml b/.buildkite/amd/test-amd-ready.yml index e46b559f3..ae64e7c4b 100644 --- a/.buildkite/amd/test-amd-ready.yml +++ b/.buildkite/amd/test-amd-ready.yml @@ -328,7 +328,10 @@ steps: - export VLLM_ALLOW_LONG_MAX_MODEL_LEN="1" - timeout 30m pytest -s -v tests/e2e/online_serving/test_qwen3_tts_customvoice.py -m "core_model" --run-level "core_model" - - label: "CosyVoice3-TTS E2E Test" + # Keep a fast model/server integration check in the blocking lane. The + # full default-backend suite remains in the nightly pipeline because a + # cold ROCm AITER MHA build can add more than 25 minutes to this job. + - label: "CosyVoice3-TTS E2E Smoke (SDPA)" agent_pool: mi300_1 depends_on: amd-build mirror_hardwares: [amdproduction] @@ -340,7 +343,8 @@ steps: - exit_status: 134 limit: 1 commands: - - pytest -s -v tests/e2e/online_serving/test_cosyvoice3_tts_expansion.py -m "slow" --run-level "core_model" + - export DIFFUSION_ATTENTION_BACKEND=TORCH_SDPA + - pytest -s -v tests/e2e/online_serving/test_cosyvoice3_tts_expansion.py::test_voice_clone_zh_002 -m "slow" --run-level "core_model" # TODO: Bagel test on ROCm is very unstable. @tjtanaa # Need to debug before reneable numerical changes across large PRs diff --git a/tests/buildkite/test_amd_pipeline.py b/tests/buildkite/test_amd_pipeline.py index fac4f8c29..36df7ac3e 100644 --- a/tests/buildkite/test_amd_pipeline.py +++ b/tests/buildkite/test_amd_pipeline.py @@ -67,11 +67,31 @@ def test_ready_diffusion_cpu_suite_is_sharded() -> None: assert "--shard-id=$$BUILDKITE_PARALLEL_JOB" in pytest_command -def test_cosyvoice_gpu_abort_gets_one_fresh_job_retry() -> None: - step = _find_step("CosyVoice3-TTS E2E Test", AMD_READY_PIPELINE) +def test_cosyvoice_ready_smoke_uses_sdpa() -> None: + step = _find_step("CosyVoice3-TTS E2E Smoke (SDPA)", AMD_READY_PIPELINE) + assert step["grade"] == "Blocking" assert step["retry"] == {"automatic": [{"exit_status": 134, "limit": 1}]} + assert "export DIFFUSION_ATTENTION_BACKEND=TORCH_SDPA" in step["commands"] + pytest_command = next(command for command in step["commands"] if "pytest" in command) + assert "tests/e2e/online_serving/test_cosyvoice3_tts_expansion.py::test_voice_clone_zh_002" in pytest_command + + +def test_cosyvoice_full_default_backend_suite_runs_nightly() -> None: + step = _find_step("CosyVoice3-TTS E2E Test", AMD_NIGHTLY_PIPELINE) + + assert step["grade"] == "NonBlocking" + assert step["timeout_in_minutes"] == 90 + assert step["retry"] == {"automatic": [{"exit_status": 134, "limit": 1}]} + assert all("DIFFUSION_ATTENTION_BACKEND" not in command for command in step["commands"]) + + pytest_command = next(command for command in step["commands"] if "pytest" in command) + assert "tests/e2e/online_serving/test_cosyvoice3_tts_expansion.py" in pytest_command + assert "::" not in pytest_command + + +def test_amd_template_preserves_step_retry_policy() -> None: template = AMD_TEMPLATE.read_text(encoding="utf-8") # Both grouped and top-level AMD steps must preserve an explicit retry # policy when the source suite is rendered into the uploaded pipeline. diff --git a/tests/model_executor/models/cosyvoice3/test_cosyvoice3_components.py b/tests/model_executor/models/cosyvoice3/test_cosyvoice3_components.py index 46255110c..56f9c9694 100644 --- a/tests/model_executor/models/cosyvoice3/test_cosyvoice3_components.py +++ b/tests/model_executor/models/cosyvoice3/test_cosyvoice3_components.py @@ -260,6 +260,37 @@ class TestDiTAttention: assert attention.to_k.out_features == 512 assert attention.to_v.out_features == 512 + def test_embedded_dit_honors_diffusion_attention_backend(self, monkeypatch): + """The LLM-hosted code2wav DiT must consume the diffusion env override.""" + from vllm_omni.diffusion.config import get_current_diffusion_config_or_none + from vllm_omni.model_executor.models.cosyvoice3.cosyvoice3_code2wav import _build_dit_estimator + + monkeypatch.setenv("DIFFUSION_ATTENTION_BACKEND", "TORCH_SDPA") + + estimator = _build_dit_estimator( + { + "dim": 32, + "depth": 1, + "heads": 2, + "dim_head": 16, + "dropout": 0.0, + "ff_mult": 2, + "mel_dim": 8, + "mu_dim": 8, + "spk_dim": 4, + "out_channels": 8, + "static_chunk_size": 4, + "num_decoding_left_chunks": 1, + } + ) + + attention = estimator.transformer_blocks[0].attn.attn + assert attention.backend_pref == "TORCH_SDPA" + assert attention.backend_explicit is True + assert attention.attn_backend is not None + assert attention.attn_backend.get_name() == "SDPA" + assert get_current_diffusion_config_or_none() is None + @pytest.mark.core_model @pytest.mark.cpu def test_forward_passes_2d_mask_without_casting_qkv(self): diff --git a/tests/model_executor/models/cosyvoice3/test_cosyvoice3_incremental_hift.py b/tests/model_executor/models/cosyvoice3/test_cosyvoice3_incremental_hift.py index 91b322864..1dd0799ef 100644 --- a/tests/model_executor/models/cosyvoice3/test_cosyvoice3_incremental_hift.py +++ b/tests/model_executor/models/cosyvoice3/test_cosyvoice3_incremental_hift.py @@ -39,7 +39,10 @@ CHUNK_LEN = 24 TOTAL_MEL = 96 SPM = 480 # samples per mel frame for the small test HiFT (8*5*3*4), matching the real model -LONG_TOTAL_MEL = 400 # long enough that the 64-frame window actually truncates +WINDOW_LEN = 64 +# Six chunks exercise three updates after history exceeds the bounded window, +# without spending minutes recomputing an unnecessarily long synthetic signal. +LONG_TOTAL_MEL = 144 PCM16_LSB = 1.0 / 32767.0 # smallest representable PCM16 difference # Worst observed deviation is 1.97e-7 (~2x float32 eps); atol=1e-6 keeps 5x headroom. @@ -189,16 +192,19 @@ def test_incremental_hift_bounded_window_is_close(config): def test_incremental_hift_matches_streaming_reference_within_tolerance(config): """Windowed output matches the full-cumulative reference within float32 rounding. - The load-bearing correctness test: 400 mel frames far exceeds the 64-frame - window, so truncation genuinely exercises the phase/noise carry. Float64 - agrees to ~1e-16, confirming the residual here is rounding, not truncation. + The load-bearing correctness test crosses the 64-frame window repeatedly, + so truncation genuinely exercises the phase/noise carry. Float64 agrees to + ~1e-16, confirming the residual here is rounding, not truncation. """ hift = _make_hift(config) - model = _make_model(hift, window_len=64) # the real _hift_window_len + model = _make_model(hift, window_len=WINDOW_LEN) # the real _hift_window_len chunks = _chunks(total_mel=LONG_TOTAL_MEL) trim = int(hift.f0_predictor.condnet[0].causal_padding) - assert LONG_TOTAL_MEL > 64 + trim + CHUNK_LEN # window must actually truncate + truncating_steps = sum( + history_len > WINDOW_LEN + trim for history_len in range(CHUNK_LEN, LONG_TOTAL_MEL, CHUNK_LEN) + ) + assert truncating_steps >= 3 # exercise repeated truncation, not only the boundary full = _full_reference(model, chunks) incr = _incremental(model, chunks) diff --git a/vllm_omni/model_executor/models/cosyvoice3/cosyvoice3_code2wav.py b/vllm_omni/model_executor/models/cosyvoice3/cosyvoice3_code2wav.py index 1713467c8..b35aefe52 100644 --- a/vllm_omni/model_executor/models/cosyvoice3/cosyvoice3_code2wav.py +++ b/vllm_omni/model_executor/models/cosyvoice3/cosyvoice3_code2wav.py @@ -10,7 +10,8 @@ This module contains the code2wav (token-to-waveform) stage which uses: """ from collections import Counter -from typing import TypedDict, cast +from collections.abc import Mapping +from typing import Any, TypedDict, cast import numpy as np import torch @@ -18,6 +19,8 @@ import torch.nn as nn from omegaconf import DictConfig from vllm.logger import init_logger +from vllm_omni.diffusion.config import get_current_diffusion_config_or_none, set_current_diffusion_config +from vllm_omni.diffusion.data import OmniDiffusionConfig from vllm_omni.diffusion.models.cosyvoice3_audio.cosyvoice3_dit import DiT from vllm_omni.model_executor.models.cosyvoice3.code2wav_core.cfm import ( CausalConditionalCFM, @@ -37,6 +40,23 @@ from vllm_omni.transformers_utils.configs.cosyvoice3 import CosyVoice3Config logger = init_logger(__name__) +def _build_dit_estimator(estimator_config: Mapping[str, Any]) -> DiT: + """Build CosyVoice's embedded DiT with diffusion backend configuration. + + The code2wav model runs inside an LLM-generation worker, so it does not go + through the diffusion model loader that normally installs an + ``OmniDiffusionConfig`` during model construction. Install a local default + config when needed so ``DIFFUSION_ATTENTION_BACKEND`` and the platform + default are resolved by the same path as standalone diffusion models. + """ + if get_current_diffusion_config_or_none() is not None: + return DiT(**estimator_config) + + diffusion_config = OmniDiffusionConfig() + with set_current_diffusion_config(diffusion_config): + return DiT(**estimator_config) + + class StreamingFlowItem(TypedDict, total=False): """One entry of the batched-streaming item list passed to forward_streaming_batch.""" @@ -71,7 +91,7 @@ class CosyVoice3Code2Wav(nn.Module): cfm_params = DictConfig(decoder_cfg["cfm_params"]) # DiT estimator using diffusion attention (Flash/Sage/SDPA backends) - estimator = DiT(**decoder_cfg["estimator"]) + estimator = _build_dit_estimator(decoder_cfg["estimator"]) decoder = CausalConditionalCFM( in_channels=decoder_cfg["in_channels"],