[Model] Default MOSS Local 1.5 to adaptive CUDA MRV2 with MPS (#8438)
Signed-off-by: Sy03 <1370724210@qq.com>
This commit is contained in:
30 files changed
+804
-148
No files matched your search
@@ -598,8 +598,9 @@ the intended GPU, and warm the complete pipeline before measuring performance.
|
||||
|
||||
## MOSS-TTS Local 1.5 with Model Runner V2
|
||||
|
||||
`OpenMOSS-Team/MOSS-TTS-Local-Transformer-v1.5` can opt into CUDA MRV2 using
|
||||
the shared runtime introduced for Qwen3-TTS:
|
||||
`OpenMOSS-Team/MOSS-TTS-Local-Transformer-v1.5` defaults to CUDA MRV2 using
|
||||
the shared runtime introduced for Qwen3-TTS. To select the C64 profile without
|
||||
MPS explicitly:
|
||||
|
||||
```bash
|
||||
vllm serve OpenMOSS-Team/MOSS-TTS-Local-Transformer-v1.5 --omni \
|
||||
@@ -607,7 +608,7 @@ vllm serve OpenMOSS-Team/MOSS-TTS-Local-Transformer-v1.5 --omni \
|
||||
```
|
||||
|
||||
This profile inherits the batching, codec graph buckets and 1-frame/15-frame
|
||||
chunk geometry from `moss_tts_local.yaml`, and selects V2 for both the Local
|
||||
chunk geometry from `moss_tts_local_v1.yaml`, and selects V2 for both the Local
|
||||
Talker and codec stages. The Local depth predictor exposes the MRV2 `mtp`
|
||||
capabilities while retaining its V1 `talker_mtp` implementation, sampling
|
||||
defaults and explicit request-seed handling. Codec chunks use the native data
|
||||
@@ -631,8 +632,12 @@ guarantee that 128 maximum-length prompts fit simultaneously. See the
|
||||
[MOSS recipe](gh-file:recipes/OpenMOSS/MOSS-TTS.md#local-15-mrv2-and-slot-attention)
|
||||
for activation, backend comparisons, memory requirements and benchmark commands.
|
||||
|
||||
Omitting `--deploy-config`, or selecting `moss_tts_local.yaml`, retains V1.
|
||||
NPU, XPU, ROCm and MUSA overrides also retain V1. This profile does not enable
|
||||
Omitting `--deploy-config` selects the C128 system profile on CUDA GPUs with
|
||||
at least 140 GiB memory and MPS available. Smaller GPUs or a failed memory
|
||||
query use C64 with utilization-based memory budgets; missing MPS selects
|
||||
C64 without MPS. Selecting `moss_tts_local.yaml` explicitly uses C64 with MPS.
|
||||
Use `moss_tts_local_v1.yaml` for the previous V1 deployment.
|
||||
NPU, XPU, ROCm and MUSA overrides retain V1. These profiles do not enable
|
||||
MRV2 for MOSS Delay, Realtime or Nano. Local 1.5 outputs 48 kHz stereo audio;
|
||||
set `VLLM_OMNI_BENCH_AUDIO_SAMPLE_RATE=48000` and
|
||||
`VLLM_OMNI_BENCH_AUDIO_CHANNELS=2` when benchmarking raw PCM.
|
||||
@@ -640,5 +645,5 @@ set `VLLM_OMNI_BENCH_AUDIO_SAMPLE_RATE=48000` and
|
||||
Event-driven orchestration remains independently selectable with
|
||||
`VLLM_OMNI_EVENT_DRIVEN_ORCH=0` or `1`. Keep the runner and deployment identical
|
||||
when comparing these modes. Model-runner selection does not change the
|
||||
orchestration default or enable experimental reference encoding, chunk ramps,
|
||||
generation-output draining or MPS.
|
||||
orchestration default. MPS, codec dispatch and graph settings belong to the
|
||||
deployment profile; the automatic CUDA default selects MPS when available.
|
||||
@@ -35,9 +35,9 @@ the requested `input` speech. Without a nonblank `ref_text`, Local uses
|
||||
audio-reference generation. Reference transcripts must match the reference
|
||||
audio; they are not style instructions.
|
||||
|
||||
For the optional CUDA MRV2 runner, see the
|
||||
For the CUDA MRV2 runner and platform fallbacks, see the
|
||||
[Local 1.5 deployment profile](../../docs/configuration/stage_configs.md#moss-tts-local-15-with-model-runner-v2).
|
||||
The default Local deployment continues to use V1.
|
||||
Local 1.5 defaults to MRV2 on CUDA and retains V1 on other platforms.
|
||||
|
||||
## References
|
||||
|
||||
@@ -159,8 +159,52 @@ curl -X POST http://localhost:8091/v1/audio/speech \
|
||||
|
||||
## Local 1.5 MRV2 and slot attention
|
||||
|
||||
`MOSS-TTS-Local-Transformer-v1.5` supports the native CUDA MRV2 pipeline with
|
||||
an explicit deploy profile. The default Local profile continues to use V1.
|
||||
`MOSS-TTS-Local-Transformer-v1.5` defaults to the native MRV2 pipeline on
|
||||
CUDA, with the original Local projection and sampler. When
|
||||
`nvidia-cuda-mps-control` is on `PATH`, it starts private full-quota MPS.
|
||||
GPUs with at least 140 GiB total memory use the C128 system profile with
|
||||
prefix caching, Triton backbone attention, bounded codec first-chunk decode,
|
||||
native Torch sampler compilation and dense codec graph buckets through 128. Its Talker KV budget is 32 GiB; smaller GPUs or a failed memory
|
||||
query use C64 with utilization-based memory budgets. When the MPS executable
|
||||
is unavailable, the automatic default is C64 MRV2 without MPS. NPU, XPU,
|
||||
ROCm and MUSA retain V1. Explicit deploy configs override automatic selection.
|
||||
|
||||
```bash
|
||||
CUDA_VISIBLE_DEVICES=0 OMP_NUM_THREADS=1 \
|
||||
vllm serve OpenMOSS-Team/MOSS-TTS-Local-Transformer-v1.5 --omni \
|
||||
--stage-init-timeout 1200 --init-timeout 1500
|
||||
```
|
||||
|
||||
Use `--deploy-config vllm_omni/deploy/moss_tts_local_v1.yaml` to select the
|
||||
previous V1 profile explicitly. C128's batch-prefill and direct-token switches
|
||||
remain confined to the throughput profile; they are not enabled in C64.
|
||||
|
||||
The same `moss_tts_local_mrv2_optimized.yaml` system profile can be selected
|
||||
explicitly. It retains the original Local depth projection and sampling algorithm.
|
||||
The codec admits fast first chunks below 32 active streams; crowded streams use
|
||||
the regular decoder with a dispatch target of 16 and a maximum wait of 6 ms.
|
||||
This preserves the low-load first-audio path while coalescing high-load work.
|
||||
`local_compile_audio_sampler: true` in the Talker HF overrides compiles the native
|
||||
Torch sampler; explicit request generators use the original helper. Set the
|
||||
override to `false` in a deployment file to disable that compilation. The setting
|
||||
is specific to this C128 system profile.
|
||||
Both stages share one GPU, with a 32 GiB Talker KV budget; this profile requires H200-class memory and
|
||||
`nvidia-cuda-mps-control` on `PATH`.
|
||||
|
||||
```bash
|
||||
CUDA_VISIBLE_DEVICES=0 OMP_NUM_THREADS=1 \
|
||||
vllm serve OpenMOSS-Team/MOSS-TTS-Local-Transformer-v1.5 --omni \
|
||||
--deploy-config vllm_omni/deploy/moss_tts_local_mrv2_optimized.yaml \
|
||||
--stage-init-timeout 1200 --init-timeout 1500
|
||||
```
|
||||
|
||||
Evaluate WER, speaker similarity and streaming latency for your workload;
|
||||
throughput gains do not establish quality equivalence. Do not apply partial
|
||||
MPS SM quotas: BF16 GEMM outputs were observed incomplete in that configuration
|
||||
on the validation environment. MPS uses an owned control socket or an
|
||||
explicitly supplied operator socket; only the owned daemon is stopped at shutdown.
|
||||
|
||||
The explicit C64 MRV2 and capped-C128 throughput profiles remain available.
|
||||
Both profiles below preserve 1-frame initial and 15-frame steady codec chunks
|
||||
and the model's sampling defaults.
|
||||
|
||||
|
||||
@@ -2985,6 +2985,9 @@ class TestPlatformOverrides:
|
||||
("moss_tts_local_mrv2.yaml", "moss_tts_local"),
|
||||
("moss_tts_local_mrv2_high_concurrency.yaml", "moss_tts_local"),
|
||||
("moss_tts_local_mrv2_low_latency.yaml", "moss_tts_local"),
|
||||
("moss_tts_local_mrv2_optimized.yaml", "moss_tts_local"),
|
||||
("moss_tts_local.yaml", "moss_tts_local"),
|
||||
("moss_tts_local_mrv2_high_concurrency_mps.yaml", "moss_tts_local"),
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize("platform", ["cuda", "npu", "xpu", "rocm", "musa"])
|
||||
@@ -3004,9 +3007,142 @@ class TestPlatformOverrides:
|
||||
# v2 only engages the native plane on stages declaring support.
|
||||
assert all(ps.supports_native_mrv2_data_plane for ps in pipeline.stages)
|
||||
|
||||
@pytest.mark.parametrize("memory_gib", [24, 80, 139, 140, 141, None])
|
||||
@pytest.mark.parametrize("mps_available", [False, True])
|
||||
def test_moss_local_cuda_default_selects_mrv2_capacity_and_mps(self, monkeypatch, memory_gib, mps_available):
|
||||
from vllm.platforms import current_platform
|
||||
|
||||
from vllm_omni.platforms import current_omni_platform
|
||||
|
||||
monkeypatch.setattr(current_omni_platform, "device_name", "cuda")
|
||||
|
||||
def memory():
|
||||
if memory_gib is None:
|
||||
raise RuntimeError("NVML unavailable")
|
||||
return memory_gib * 1024**3
|
||||
|
||||
monkeypatch.setattr(current_platform, "get_device_total_memory", memory)
|
||||
monkeypatch.setattr(
|
||||
"shutil.which", lambda command: "/usr/bin/nvidia-cuda-mps-control" if mps_available else None
|
||||
)
|
||||
pipeline = resolve_pipeline_config("moss_tts_local")
|
||||
high_capacity = mps_available and memory_gib is not None and memory_gib >= 140
|
||||
capacity = 128 if high_capacity else 64
|
||||
config = VllmOmniConfig.from_pipeline_config(pipeline)
|
||||
talker, codec = config.stage_configs
|
||||
assert talker.scheduler_config.max_num_seqs == codec.scheduler_config.max_num_seqs == capacity
|
||||
assert talker.cache_config.kv_cache_memory_bytes == (32 * 1024**3 if high_capacity else None)
|
||||
assert talker.cache_config.gpu_memory_utilization == 0.60
|
||||
assert codec.cache_config.gpu_memory_utilization == 0.30
|
||||
assert talker.cache_config.enable_prefix_caching is high_capacity
|
||||
assert not codec.cache_config.enable_prefix_caching
|
||||
for stage in config.stage_configs:
|
||||
assert stage.model_config.use_v2_model_runner
|
||||
assert stage.runtime_config.cuda_mps is mps_available
|
||||
assert not stage.runtime_config.env
|
||||
overrides = talker.model_config.hf_overrides or {}
|
||||
assert overrides.get("mrv2_batch_prefill", False) is high_capacity
|
||||
assert overrides.get("mrv2_direct_tokens", False) is high_capacity
|
||||
assert overrides.get("local_compile_audio_sampler", False) is high_capacity
|
||||
|
||||
# The serving engine still consumes the legacy representation.
|
||||
stages, _ = StageConfigFactory._create_legacy_from_registry(pipeline, {})
|
||||
for stage in stages:
|
||||
assert stage.yaml_engine_args["max_num_seqs"] == capacity
|
||||
assert stage.yaml_engine_args["use_v2_model_runner"]
|
||||
assert stage.yaml_runtime.get("cuda_mps", False) is mps_available
|
||||
assert stages[0].yaml_engine_args.get("kv_cache_memory_bytes") == (32 * 1024**3 if high_capacity else None)
|
||||
assert stages[0].yaml_engine_args["enable_prefix_caching"] is high_capacity
|
||||
|
||||
@pytest.mark.parametrize("platform", ["cpu", "npu", "xpu", "rocm", "musa"])
|
||||
def test_moss_local_non_cuda_default_preserves_v1(self, monkeypatch, platform):
|
||||
from vllm.platforms import current_platform
|
||||
|
||||
from vllm_omni.platforms import current_omni_platform
|
||||
|
||||
monkeypatch.setattr(current_omni_platform, "device_name", platform)
|
||||
|
||||
def unexpected_probe(*args):
|
||||
pytest.fail("Non-CUDA selection must not probe CUDA memory or MPS")
|
||||
|
||||
monkeypatch.setattr(current_platform, "get_device_total_memory", unexpected_probe)
|
||||
monkeypatch.setattr("shutil.which", unexpected_probe)
|
||||
pipeline = resolve_pipeline_config("moss_tts_local")
|
||||
config = VllmOmniConfig.from_pipeline_config(pipeline)
|
||||
for stage in config.stage_configs:
|
||||
assert not stage.model_config.use_v2_model_runner
|
||||
assert not stage.runtime_config.cuda_mps
|
||||
assert stage.scheduler_config.max_num_seqs == 64
|
||||
assert config.stage_configs[0].cache_config.kv_cache_memory_bytes is None
|
||||
stages, _ = StageConfigFactory._create_legacy_from_registry(pipeline, {})
|
||||
assert all(not stage.yaml_engine_args["use_v2_model_runner"] for stage in stages)
|
||||
assert all(not stage.yaml_runtime.get("cuda_mps", False) for stage in stages)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"filename,runner,mps,capacity",
|
||||
[
|
||||
("moss_tts_local_v1.yaml", False, False, 64),
|
||||
("moss_tts_local_mrv2.yaml", True, False, 64),
|
||||
("moss_tts_local_mrv2_high_concurrency.yaml", True, False, 128),
|
||||
],
|
||||
)
|
||||
def test_moss_local_explicit_profile_overrides_cuda_default(self, monkeypatch, filename, runner, mps, capacity):
|
||||
from vllm.platforms import current_platform
|
||||
|
||||
from vllm_omni.platforms import current_omni_platform
|
||||
|
||||
monkeypatch.setattr(current_omni_platform, "device_name", "cuda")
|
||||
monkeypatch.setattr(current_platform, "get_device_total_memory", lambda: 141 * 1024**3)
|
||||
monkeypatch.setattr("shutil.which", lambda command: "/usr/bin/nvidia-cuda-mps-control")
|
||||
pipeline = resolve_pipeline_config("moss_tts_local")
|
||||
path = get_deploy_config_path(filename)
|
||||
config = VllmOmniConfig.from_pipeline_config(pipeline, deploy_config_path=path)
|
||||
for stage in config.stage_configs:
|
||||
assert stage.model_config.use_v2_model_runner is runner
|
||||
assert stage.runtime_config.cuda_mps is mps
|
||||
assert stage.scheduler_config.max_num_seqs == capacity
|
||||
stages, _ = StageConfigFactory._create_legacy_from_registry(pipeline, {}, deploy_config_path=path)
|
||||
for stage in stages:
|
||||
assert stage.yaml_engine_args["use_v2_model_runner"] is runner
|
||||
assert stage.yaml_runtime.get("cuda_mps", False) is mps
|
||||
assert stage.yaml_engine_args["max_num_seqs"] == capacity
|
||||
|
||||
@pytest.mark.parametrize("platform", ["cuda", "npu", "xpu", "rocm", "musa"])
|
||||
def test_moss_local_system_profile_reaches_native_runner_and_cuda_only_mps(self, platform):
|
||||
pipeline = resolve_pipeline_config("moss_tts_local")
|
||||
path = Path(get_deploy_config_path("moss_tts_local_mrv2_optimized.yaml"))
|
||||
deploy = _apply_platform_overrides(load_deploy_config(path), platform=platform)
|
||||
stages = merge_pipeline_deploy(pipeline, deploy)
|
||||
is_cuda = platform == "cuda"
|
||||
assert deploy.cuda_mps is is_cuda
|
||||
assert all(stage.yaml_runtime.get("cuda_mps", False) is is_cuda for stage in stages)
|
||||
assert all(stage.yaml_engine_args["use_v2_model_runner"] is is_cuda for stage in stages)
|
||||
if is_cuda:
|
||||
args = stages[0].yaml_engine_args
|
||||
assert args["enable_prefix_caching"] is True
|
||||
assert args["hf_overrides"]["mrv2_gpu_slot_state"] is True
|
||||
assert args["hf_overrides"]["mrv2_batch_prefill"] is True
|
||||
assert args["hf_overrides"]["mrv2_direct_tokens"] is True
|
||||
assert deploy.connectors["shm"]["extra"]["codec_first_chunk_fast_path"] == 1
|
||||
assert args["hf_overrides"]["local_compile_audio_sampler"] is True
|
||||
extra = deploy.connectors["shm"]["extra"]
|
||||
assert extra["codec_first_chunk_max_active_streams"] == 32
|
||||
assert extra["generation_min_batch_size"] == 16
|
||||
assert extra["generation_max_wait_ms"] == 6
|
||||
assert not stages[0].yaml_runtime.get("env")
|
||||
else:
|
||||
assert all(not stage.yaml_engine_args["enable_prefix_caching"] for stage in stages)
|
||||
assert all(not stage.yaml_runtime.get("env") for stage in stages)
|
||||
|
||||
def test_platform_mps_rejects_non_boolean(self):
|
||||
deploy = load_deploy_config(get_deploy_config_path("moss_tts_local_mrv2_optimized.yaml"))
|
||||
deploy.platforms["cuda"]["cuda_mps"] = "true"
|
||||
with pytest.raises(ValueError, match="platform cuda_mps must be a boolean"):
|
||||
_apply_platform_overrides(deploy, platform="cuda")
|
||||
|
||||
def test_moss_local_mrv2_preserves_base_profile_and_variant_scope(self):
|
||||
pipeline = resolve_pipeline_config("moss_tts_local")
|
||||
base_path = Path(get_deploy_config_path(pipeline.default_deploy_config_name))
|
||||
base_path = Path(get_deploy_config_path("moss_tts_local_v1.yaml"))
|
||||
candidate_path = Path(get_deploy_config_path("moss_tts_local_mrv2.yaml"))
|
||||
base = load_deploy_config(base_path)
|
||||
candidate = load_deploy_config(candidate_path)
|
||||
@@ -3052,7 +3188,7 @@ class TestPlatformOverrides:
|
||||
assert compilation["cudagraph_mode"] == "FULL"
|
||||
assert compilation["cudagraph_capture_sizes"] == [1, 2, 4, 6, 8, 12, 16, 24, 32, 64, 128]
|
||||
assert compilation["inductor_compile_config"] == {"combo_kernels": False, "benchmark_combo_kernel": False}
|
||||
base_path = Path(get_deploy_config_path("moss_tts_local.yaml"))
|
||||
base_path = Path(get_deploy_config_path("moss_tts_local_v1.yaml"))
|
||||
for platform in ("npu", "xpu", "rocm", "musa"):
|
||||
actual = _apply_platform_overrides(load_deploy_config(path), platform=platform)
|
||||
expected = _apply_platform_overrides(load_deploy_config(base_path), platform=platform)
|
||||
|
||||
@@ -1887,7 +1887,7 @@ def test_from_pipeline_config_rejects_reserved_diffusion_kv_mode(tmp_path):
|
||||
|
||||
@pytest.mark.parametrize("source", ["default", "topology", "deploy", "stage-cli"])
|
||||
@pytest.mark.parametrize("key_container", [list, tuple])
|
||||
def test_diffusion_stage_payload_keys_roundtrip(source, key_container):
|
||||
def test_diffusion_stage_payload_keys_roundtrip(source, key_container, tmp_path):
|
||||
from vllm_omni.diffusion.data import OmniDiffusionConfig
|
||||
from vllm_omni.engine.stage_init_utils import build_engine_args_dict_from_omni_stage_config
|
||||
|
||||
@@ -1920,7 +1920,7 @@ def test_diffusion_stage_payload_keys_roundtrip(source, key_container):
|
||||
legacy_stage = merge_pipeline_deploy(pipeline, deploy)[0]
|
||||
legacy_args = {**legacy_stage.yaml_engine_args, **(override_keys if source == "stage-cli" else {})}
|
||||
restored_stage = ForkingPickler.loads(ForkingPickler.dumps(stage))
|
||||
engine_args = build_engine_args_dict_from_omni_stage_config(restored_stage, model="test-model")
|
||||
engine_args = build_engine_args_dict_from_omni_stage_config(restored_stage, model=str(tmp_path))
|
||||
diffusion_kwargs = omni_config_module.extract_diffusion_stage_config_kwargs(
|
||||
engine_args, stage_id=restored_stage.stage_id, include_engine_adapter_metadata=True
|
||||
)
|
||||
|
||||
@@ -44,10 +44,13 @@ def _config(*, native=True, stateful=True, tp=1, pp=1, extras=None, capacity=128
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("profile", ["high_concurrency", "low_latency"])
|
||||
@pytest.mark.parametrize(
|
||||
"profile,min_batch,max_wait_ms",
|
||||
[("high_concurrency", 32, 12), ("low_latency", 32, 12), ("optimized", 16, 6)],
|
||||
)
|
||||
@pytest.mark.parametrize("platform", ["cuda", "npu", "xpu", "rocm", "musa"])
|
||||
def test_moss_profile_generation_constructor_after_platform_resolution(
|
||||
construct_scheduler, monkeypatch, mocker, profile, platform
|
||||
construct_scheduler, monkeypatch, mocker, profile, min_batch, max_wait_ms, platform
|
||||
):
|
||||
# merge_pipeline_deploy owns platform resolution. Mock the detected device
|
||||
# instead of applying an override first and then resolving the worker's
|
||||
@@ -66,11 +69,12 @@ def test_moss_profile_generation_constructor_after_platform_resolution(
|
||||
warning = mocker.patch("vllm_omni.core.sched.omni_generation_scheduler.logger.warning")
|
||||
scheduler = construct_scheduler(config)
|
||||
# The raw extras persist through fallback; the live constructor must cope.
|
||||
assert extras["generation_min_batch_size"] == 32 and extras["generation_max_wait_ms"] == 12
|
||||
assert extras["generation_min_batch_size"] == min_batch
|
||||
assert extras["generation_max_wait_ms"] == max_wait_ms
|
||||
if platform == "cuda":
|
||||
assert scheduler._native_data_plane and scheduler.input_coordinator is not None
|
||||
assert scheduler._generation_min_batch_size == 32
|
||||
assert scheduler._generation_max_wait_s == 0.012
|
||||
assert scheduler._generation_min_batch_size == min_batch
|
||||
assert scheduler._generation_max_wait_s == max_wait_ms / 1000
|
||||
assert scheduler._generation_max_regular_batch == (16 if profile == "low_latency" else 0)
|
||||
warning.assert_not_called()
|
||||
if profile == "low_latency":
|
||||
|
||||
@@ -61,6 +61,7 @@ from vllm_omni.entrypoints.openai.tts_adapters.base import (
|
||||
)
|
||||
from vllm_omni.entrypoints.openai.tts_adapters.capabilities import load_supported_speakers
|
||||
from vllm_omni.entrypoints.openai.tts_adapters.ming_tts import MingTTSAdapter
|
||||
from vllm_omni.entrypoints.openai.tts_adapters.moss_tts import MossTTSAdapter
|
||||
from vllm_omni.entrypoints.openai.tts_adapters.qwen3_tts import Qwen3TTSAdapter, Qwen3TTSCodecLimitError
|
||||
from vllm_omni.entrypoints.openai.tts_adapters.voxtral import VoxtralTTSAdapter
|
||||
from vllm_omni.entrypoints.serve.utils import errors as serve_errors
|
||||
@@ -84,6 +85,42 @@ pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("sse", [False, True])
|
||||
@pytest.mark.parametrize("payload", [None, "empty", "valid"])
|
||||
@pytest.mark.asyncio
|
||||
async def test_moss_empty_stream_is_an_error(mocker, sse, payload):
|
||||
serving = OmniOpenAIServingSpeech.__new__(OmniOpenAIServingSpeech)
|
||||
serving._tts_model_type = "moss_tts"
|
||||
serving.engine_client = SimpleNamespace(
|
||||
model_config=SimpleNamespace(model="OpenMOSS-Team/MOSS-TTS-Local-Transformer-v1.5")
|
||||
)
|
||||
serving._adapter = MossTTSAdapter(SpeechServingContext(server=serving, engine_client=serving.engine_client))
|
||||
ready = mocker.patch.object(serving, "_mark_ref_audio_artifact_ready_for_request")
|
||||
discard = mocker.patch.object(serving, "_discard_ref_audio_artifact_warmup")
|
||||
|
||||
async def results():
|
||||
if payload is not None:
|
||||
audio = torch.zeros(320) if payload == "valid" else torch.empty(0)
|
||||
yield SimpleNamespace(multimodal_output={"model_outputs": audio, "sr": 24000})
|
||||
yield SimpleNamespace(multimodal_output={"model_outputs": torch.empty(0), "sr": 24000})
|
||||
|
||||
if sse:
|
||||
events = [event async for event in serving._generate_audio_sse_events(results(), "moss-empty")]
|
||||
assert any("speech.audio.done" in event for event in events) == (payload == "valid")
|
||||
assert any("speech.audio.error" in event for event in events) == (payload != "valid")
|
||||
else:
|
||||
chunks = serving._generate_audio_chunks(results(), "moss-empty", response_format="wav")
|
||||
if payload == "valid":
|
||||
output = [chunk async for chunk in chunks]
|
||||
assert len(output) == 2 and output[0].startswith(b"RIFF")
|
||||
assert len(output[1]) == 640
|
||||
else:
|
||||
with pytest.raises(TTSGenerationError, match="no audio"):
|
||||
await anext(chunks)
|
||||
assert ready.call_count == int(payload == "valid")
|
||||
assert discard.call_count == int(payload != "valid")
|
||||
|
||||
|
||||
class TestAudioMixin:
|
||||
@pytest.fixture
|
||||
def audio_mixin(self):
|
||||
|
||||
@@ -283,7 +283,7 @@ async def test_streaming_speech_does_not_count_empty_payload_as_first_packet(mon
|
||||
request_id="speech-test",
|
||||
request_arrival_ts=100.0,
|
||||
)
|
||||
assert len([chunk async for chunk in chunks]) == 2
|
||||
assert [chunk async for chunk in chunks] == [b"\0\0" * 320]
|
||||
assert metrics.ttfp_calls == [("1", "2", pytest.approx(0.25))]
|
||||
assert metrics.underrun_calls == [("1", "2", pytest.approx(0.0))]
|
||||
assert metrics.continuity_calls == [("1", "2", 100)]
|
||||
@@ -319,7 +319,11 @@ async def test_streaming_speech_does_not_finalize_continuity_on_validation_error
|
||||
def reject_generation(_tts_params, **_kwargs):
|
||||
raise RuntimeError("generation validation failed")
|
||||
|
||||
adapter = SimpleNamespace(validates_generation=True, validate_generation=reject_generation)
|
||||
adapter = SimpleNamespace(
|
||||
validates_generation=True,
|
||||
validate_generation=reject_generation,
|
||||
validate_stream_audio=lambda **_kwargs: None,
|
||||
)
|
||||
serving = _serving(metrics, adapter=adapter)
|
||||
monkeypatch.setattr("vllm_omni.entrypoints.openai.serving_speech.time.time", lambda: 100.25)
|
||||
|
||||
|
||||
@@ -318,26 +318,45 @@ def test_failed_output_handoff_retains_decoded_samples(cuda):
|
||||
fast.close()
|
||||
|
||||
|
||||
@pytest.mark.cpu
|
||||
@pytest.mark.parametrize("enabled,complete", [(False, False), (True, False), (True, True), (True, None)])
|
||||
def test_gate_waits_only_for_enabled_inflight_decode(mocker, enabled, complete):
|
||||
# A fixed GPU sleep cannot prove that an event is still pending when the
|
||||
# host reaches gate(), particularly across CUDA and ROCm implementations.
|
||||
fast = object.__new__(MossFirstChunkFastPath)
|
||||
fast._device = torch.device("cuda")
|
||||
fast._gate_main = enabled
|
||||
event = None if complete is None else mocker.Mock()
|
||||
if event is not None:
|
||||
event.query.return_value = complete
|
||||
fast._inflight = event
|
||||
stream = mocker.Mock()
|
||||
current_stream = mocker.patch("torch.cuda.current_stream", return_value=stream)
|
||||
fast.gate()
|
||||
if enabled and complete is False:
|
||||
current_stream.assert_called_once_with(fast._device)
|
||||
stream.wait_event.assert_called_once_with(event)
|
||||
else:
|
||||
current_stream.assert_not_called()
|
||||
stream.wait_event.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.cuda
|
||||
def test_gate_orders_main_stream_after_inflight_fast_decode(cuda):
|
||||
def test_gate_hands_inflight_gpu_output_to_main_stream(cuda):
|
||||
fast, _, _ = _fast(cuda)
|
||||
side = torch.cuda.Stream()
|
||||
output = torch.zeros(16, device=cuda)
|
||||
side.wait_stream(torch.cuda.current_stream())
|
||||
with torch.cuda.stream(side):
|
||||
torch.cuda._sleep(200_000_000)
|
||||
output.fill_(7)
|
||||
pending = torch.cuda.Event()
|
||||
pending.record(side)
|
||||
fast._inflight = pending
|
||||
fast.gate() # disabled: no wait
|
||||
unordered = torch.cuda.Event()
|
||||
unordered.record()
|
||||
fast._gate_main = True
|
||||
fast.gate()
|
||||
ordered = torch.cuda.Event()
|
||||
ordered.record()
|
||||
unordered.synchronize()
|
||||
assert not pending.query() and not ordered.query()
|
||||
ordered.synchronize()
|
||||
assert pending.query()
|
||||
received = output.clone()
|
||||
torch.testing.assert_close(received, torch.full_like(received, 7))
|
||||
fast.close()
|
||||
|
||||
|
||||
def _cpu_fast(monkeypatch, *, scheduler=None):
|
||||
@@ -545,3 +564,41 @@ def test_platform_failure_wakes_pending_handoff(monkeypatch):
|
||||
assert fast._closed
|
||||
with pytest.raises(RuntimeError, match="decode failed"):
|
||||
fast.order_after(0)
|
||||
|
||||
|
||||
@pytest.mark.cpu
|
||||
@pytest.mark.tts
|
||||
@pytest.mark.parametrize("limit", [1, 2])
|
||||
def test_congested_admission_preserves_routes_and_slots_then_recovers(monkeypatch, limit):
|
||||
import sys
|
||||
from functools import partial
|
||||
|
||||
helpers = sys.modules[__name__]
|
||||
from vllm_omni.model_executor.models.moss_tts.first_chunk_fast_path import MossFirstChunkFastPath
|
||||
|
||||
monkeypatch.setattr(helpers, "MossFirstChunkFastPath", partial(MossFirstChunkFastPath, max_active_streams=limit))
|
||||
fast, session, sink, _ = helpers._cpu_fast(monkeypatch)
|
||||
calls = []
|
||||
|
||||
class Sink:
|
||||
def prepare(self, ids):
|
||||
calls.append(ids)
|
||||
return sink.prepare(ids)
|
||||
|
||||
fast._sink = Sink()
|
||||
thread = threading.Thread()
|
||||
monkeypatch.setattr(thread, "is_alive", lambda: True)
|
||||
fast._thread = thread
|
||||
slots = {f"busy{i}": session.acquire() for i in range(limit)}
|
||||
before_slots, before_free = slots.copy(), session.free.copy()
|
||||
codes = torch.tensor([1, 2])
|
||||
assert not fast.submit("r", "r", codes, slots)
|
||||
assert slots == before_slots and session.free == before_free
|
||||
assert not calls and not fast._decoded and not fast._handoffs and fast._jobs.empty()
|
||||
released = slots.pop("busy0")
|
||||
session.release(released)
|
||||
assert fast.submit("r", "r", codes, slots)
|
||||
codes.fill_(99)
|
||||
job = fast._jobs.get_nowait()
|
||||
assert job.slot == slots["r"] and calls == [["r"]]
|
||||
assert job.codes.tolist() == [[1], [2]]
|
||||
@@ -0,0 +1,48 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
"""Deployment sampler settings override the legacy environment switch."""
|
||||
|
||||
import pytest
|
||||
from transformers import GPT2Config
|
||||
|
||||
from vllm_omni.model_executor.models.moss_tts import modeling_moss_tts_local_depth as depth
|
||||
|
||||
pytestmark = [pytest.mark.core_model, pytest.mark.tts, pytest.mark.cpu]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"configured,env,expected", [(None, "0", False), (None, "1", True), (True, "0", True), (False, "1", False)]
|
||||
)
|
||||
def test_sampler_compile_setting_and_idempotent_setup(monkeypatch, configured, env, expected):
|
||||
monkeypatch.setenv("VLLM_OMNI_MOSS_LOCAL_COMPILE_AUDIO_SAMPLER", env)
|
||||
monkeypatch.setattr(depth.current_omni_platform, "supports_torch_inductor", lambda: True)
|
||||
calls = []
|
||||
|
||||
def compile_fn(fn, **kwargs):
|
||||
calls.append((fn, kwargs))
|
||||
return fn
|
||||
|
||||
monkeypatch.setattr(depth.torch, "compile", compile_fn)
|
||||
model = depth.MossTTSLocalDepthTransformer(
|
||||
GPT2Config(n_embd=80, n_head=1, n_inner=160), compile_audio_sampler=configured
|
||||
)
|
||||
model.setup_compile()
|
||||
model.setup_compile()
|
||||
assert len(calls) == 1 + expected
|
||||
assert (model._compiled_audio_sampler is depth._sample_token) is expected
|
||||
sampler_calls = [kwargs for fn, kwargs in calls if fn is depth._sample_token]
|
||||
assert len(sampler_calls) == int(expected)
|
||||
if expected:
|
||||
assert sampler_calls[0] == {"fullgraph": True, "dynamic": True, "options": {"fallback_random": True}}
|
||||
|
||||
|
||||
def test_sampler_compile_setting_respects_platform_support(monkeypatch):
|
||||
monkeypatch.setattr(depth.current_omni_platform, "supports_torch_inductor", lambda: False)
|
||||
|
||||
def forbidden(*args, **kwargs):
|
||||
raise AssertionError("compile called on unsupported platform")
|
||||
|
||||
monkeypatch.setattr(depth.torch, "compile", forbidden)
|
||||
model = depth.MossTTSLocalDepthTransformer(GPT2Config(n_embd=80, n_head=1, n_inner=160), compile_audio_sampler=True)
|
||||
model.setup_compile()
|
||||
assert model._compiled_audio_sampler is None
|
||||
@@ -40,13 +40,17 @@ def _model(device):
|
||||
model.audio_assistant_slot_token_id, model.im_end_token_id = 2, 3
|
||||
model.talker_mtp_output_key = ("audio_codes", "current")
|
||||
model.talker_mtp_graph_safe = False
|
||||
model.talker_mtp_accepts_per_row_generators = True
|
||||
model.gpu_resident_buffer_keys = {("hidden_states", "last"), ("audio_codes", "current")}
|
||||
model.audio_lm_heads = model.audio_embeddings = model.local_text_lm_head = None
|
||||
model._audio_embed = lambda codes: codes[:, :1].expand(-1, 4).to(dtype) / 8
|
||||
model.force_stop = False
|
||||
|
||||
def frame(hidden, *_args, generator=None, temperature=None, top_k=None, top_p=None, **_kwargs):
|
||||
def frame(hidden, *_args, generator=None, generators=None, temperature=None, top_k=None, top_p=None, **_kwargs):
|
||||
assert (temperature, top_k, top_p) == (1.7, 25, 0.8)
|
||||
if generators is not None:
|
||||
noise = torch.cat([torch.randint(0, 8, (1, 2), device=device, generator=g) for g in generators])
|
||||
else:
|
||||
noise = torch.randint(0, 8, (hidden.shape[0], 2), device=device, generator=generator)
|
||||
codes = (hidden[:, :2].mul(16).long() + noise) % 8
|
||||
return torch.full((hidden.shape[0],), not model.force_stop, device=device), codes
|
||||
@@ -85,11 +89,11 @@ def _admit(state, slot, name, seed):
|
||||
state.intermediate_buffer.buffers[slot]["codes"] = {"ref": torch.tensor([[1, 2], [3, 4], [5, 6], [2, 3]])}
|
||||
|
||||
|
||||
def _batch(device, slots, counts):
|
||||
def _batch(device, slots, counts, index_dtype=torch.int32):
|
||||
starts = np.array([0, *np.cumsum(counts)], dtype=np.int32)
|
||||
return SimpleNamespace(
|
||||
idx_mapping_np=np.array(slots),
|
||||
idx_mapping=torch.tensor(slots, device=device),
|
||||
idx_mapping=torch.tensor(slots, device=device, dtype=index_dtype),
|
||||
num_reqs=len(slots),
|
||||
num_tokens=int(starts[-1]),
|
||||
num_scheduled_tokens=np.array(counts),
|
||||
@@ -122,7 +126,8 @@ def _step(state, batch, req_states, dispatcher=None):
|
||||
|
||||
@pytest.mark.parametrize("seed", [None, 17])
|
||||
@pytest.mark.parametrize("batch_prefill", [False, True])
|
||||
def test_slot_matches_canonical_mixed_prefill_reorder_stop_and_reuse(device, seed, batch_prefill):
|
||||
@pytest.mark.parametrize("index_dtype", [torch.int32, torch.int64])
|
||||
def test_slot_matches_canonical_mixed_prefill_reorder_stop_and_reuse(device, seed, batch_prefill, index_dtype):
|
||||
reference, candidate = (_state(cls, device) for cls in (OmniModelState, MossLocalModelState))
|
||||
candidate._batch_prefill = batch_prefill
|
||||
for state in (reference, candidate):
|
||||
@@ -154,7 +159,7 @@ def test_slot_matches_canonical_mixed_prefill_reorder_stop_and_reuse(device, see
|
||||
for state in (reference, candidate):
|
||||
state.model.force_stop = index == 3
|
||||
torch.manual_seed(91 + index)
|
||||
outputs.append(_step(state, _batch(device, slots, counts), req_states))
|
||||
outputs.append(_step(state, _batch(device, slots, counts, index_dtype), req_states))
|
||||
a, b = outputs
|
||||
torch.testing.assert_close(a[0], b[0], rtol=0, atol=0)
|
||||
torch.testing.assert_close(a[2], b[2], rtol=0, atol=0)
|
||||
@@ -190,6 +195,23 @@ def test_slot_honors_external_stopping_control(device):
|
||||
assert output[1]["codes"]["audio"][0].tolist() == [[8, 8]]
|
||||
|
||||
|
||||
@pytest.mark.cpu
|
||||
@pytest.mark.parametrize("state_cls", [OmniModelState, MossLocalModelState])
|
||||
def test_local_seeded_mtp_keeps_per_row_generators_in_one_batch(state_cls):
|
||||
state = _state(state_cls, torch.device("cpu"))
|
||||
generators = [torch.Generator().manual_seed(seed) for seed in (17, 29)]
|
||||
inputs = [torch.zeros(2, dtype=torch.long), *[torch.ones(2, 4) for _ in range(3)]]
|
||||
frame = MagicMock(wraps=state.model.local_transformer.generate_frame)
|
||||
state.model.local_transformer.generate_frame = frame
|
||||
|
||||
state._call_mtp_with_sampling(*inputs, buffers=[{}, {}], req_ids=["first", "second"], generators=generators)
|
||||
|
||||
frame.assert_called_once()
|
||||
assert frame.call_args.args[0].shape[0] == 2
|
||||
assert frame.call_args.kwargs["generators"] == generators
|
||||
assert frame.call_args.kwargs["generator"] is None
|
||||
|
||||
|
||||
@pytest.mark.cuda
|
||||
def test_slot_mtp_graph_padding_reorder_and_owned_output():
|
||||
if not torch.cuda.is_available():
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
"""Reference batching preserves coordinates and asynchronous source ownership."""
|
||||
|
||||
from types import SimpleNamespace
|
||||
from dataclasses import dataclass
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
@@ -15,6 +15,12 @@ from vllm_omni.model_executor.models.moss_tts.modeling_moss_tts_talker import Mo
|
||||
pytestmark = pytest.mark.core_model
|
||||
|
||||
|
||||
@dataclass
|
||||
class _PrefillPositions:
|
||||
prompt_len: np.ndarray
|
||||
num_computed_tokens: np.ndarray
|
||||
|
||||
|
||||
@pytest.mark.cpu
|
||||
@pytest.mark.parametrize("flat", [False, True])
|
||||
def test_prefill_reuses_text_embeddings_and_noncontiguous_reference_offsets(mocker, flat):
|
||||
@@ -28,7 +34,8 @@ def test_prefill_reuses_text_embeddings_and_noncontiguous_reference_offsets(mock
|
||||
"ref_offset": offset,
|
||||
}
|
||||
batch = _batch(torch.device("cpu"), [3, 0], [2, 1])
|
||||
req = SimpleNamespace(prompt_len=np.full(5, 8), num_computed_tokens=np.zeros(5))
|
||||
req = _PrefillPositions(prompt_len=np.full(5, 8), num_computed_tokens=np.zeros(5))
|
||||
req.num_computed_tokens[[3, 0]] = [1, 2]
|
||||
embeds = state._static_inputs_embeds[:3]
|
||||
expected = state.model.embed_input_ids(batch.input_ids) + state.model._audio_embed(codes[[1, 2, 2]])
|
||||
embed = mocker.spy(state.model.model.embed_tokens, "forward")
|
||||
@@ -40,6 +47,65 @@ def test_prefill_reuses_text_embeddings_and_noncontiguous_reference_offsets(mock
|
||||
assert state.intermediate_buffer.buffers[0]["ref_offset"] == 3
|
||||
|
||||
|
||||
@pytest.mark.cpu
|
||||
@pytest.mark.parametrize("batch_prefill", [False, True])
|
||||
@pytest.mark.parametrize("cached,count", [(2, 1), (1, 2)])
|
||||
def test_cached_prefix_prefill_uses_absolute_reference_position(batch_prefill, cached, count):
|
||||
state = _state(MossLocalModelState, torch.device("cpu"))
|
||||
state._batch_prefill = batch_prefill
|
||||
codes = torch.tensor([[1, 2], [3, 4], [5, 6], [2, 3]])
|
||||
state.intermediate_buffer.buffers[0] = {"req_id": "prefix-hit", "codes": {"ref": codes}}
|
||||
batch = _batch(torch.device("cpu"), [0], [count])
|
||||
req = _PrefillPositions(prompt_len=np.full(5, 4), num_computed_tokens=np.full(5, cached))
|
||||
embeds = state._static_inputs_embeds[:count]
|
||||
expected = state.model.embed_input_ids(batch.input_ids) + state.model._audio_embed(codes[cached : cached + count])
|
||||
with torch.inference_mode():
|
||||
state.run_preprocess(batch, {"input_ids": batch.input_ids, "inputs_embeds": embeds}, req)
|
||||
torch.testing.assert_close(embeds, expected, rtol=0, atol=0)
|
||||
assert state.intermediate_buffer.buffers[0]["ref_offset"] == cached + count
|
||||
|
||||
|
||||
@pytest.mark.cpu
|
||||
def test_scalar_prefill_uses_cached_prompt_position():
|
||||
state = _state(MossLocalModelState, torch.device("cpu"))
|
||||
codes = torch.tensor([[1, 2], [3, 4], [5, 6], [2, 3]])
|
||||
ids = torch.tensor([2])
|
||||
expected = state.model.embed_input_ids(ids) + state.model._audio_embed(codes[2:3])
|
||||
with torch.inference_mode():
|
||||
_, actual, updates = state.model.preprocess(
|
||||
ids, None, codes={"ref": codes}, _omni_is_prefill=True, _omni_num_computed_tokens=2
|
||||
)
|
||||
torch.testing.assert_close(actual, expected, rtol=0, atol=0)
|
||||
assert updates["ref_offset"] == 3
|
||||
|
||||
|
||||
@pytest.mark.cpu
|
||||
@pytest.mark.parametrize("batch_prefill", [False, True])
|
||||
@pytest.mark.parametrize("omit_trailing_pad", [False, True])
|
||||
def test_prefill_chunk_crosses_reference_end(batch_prefill, omit_trailing_pad):
|
||||
state = _state(MossLocalModelState, torch.device("cpu"))
|
||||
state._batch_prefill = batch_prefill
|
||||
# codes.ref uses prompt coordinates, including PAD rows for text tokens.
|
||||
codes = torch.tensor([[8, 8], [1, 2], [3, 4], [5, 6], [8, 8], [8, 8]])
|
||||
if omit_trailing_pad:
|
||||
codes = codes[:4]
|
||||
state.model._audio_embed = lambda rows: rows[:, :1].masked_fill(rows[:, :1] == 8, 0).expand(-1, 4) / 8
|
||||
state.intermediate_buffer.buffers[0] = {"req_id": "reference-boundary", "codes": {"ref": codes}}
|
||||
req = _PrefillPositions(prompt_len=np.full(5, 6), num_computed_tokens=np.zeros(5))
|
||||
outputs = []
|
||||
with torch.inference_mode():
|
||||
for start, count in [(0, 2), (2, 4)]:
|
||||
req.num_computed_tokens[0] = start
|
||||
batch = _batch(torch.device("cpu"), [0], [count])
|
||||
embeds = state._static_inputs_embeds[:count]
|
||||
state.run_preprocess(batch, {"input_ids": batch.input_ids, "inputs_embeds": embeds}, req)
|
||||
outputs.append(embeds.clone())
|
||||
expected = state.model.embed_input_ids(torch.full((6,), 2))
|
||||
expected[1:4] += torch.tensor([1, 3, 5]).reshape(-1, 1) / 8
|
||||
torch.testing.assert_close(torch.cat(outputs), expected, rtol=0, atol=0)
|
||||
assert state.intermediate_buffer.buffers[0]["ref_offset"] == 6
|
||||
|
||||
|
||||
@pytest.mark.cuda
|
||||
def test_pending_reference_uploads_survive_staging_reuse_and_overflow():
|
||||
if not torch.cuda.is_available():
|
||||
|
||||
@@ -11,6 +11,7 @@ from typing import Any
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
from vllm import SamplingParams
|
||||
|
||||
from vllm_omni.worker_v2.delivery import DeliveryCancelledError, DeliveryState, OmniDeliveryManager
|
||||
from vllm_omni.worker_v2.native_output_worker import NativeOutputWorker
|
||||
@@ -126,6 +127,60 @@ def test_full_payload_abort_discards_partial_and_late_outputs(plane):
|
||||
assert not plane._pending_full_payload_send
|
||||
|
||||
|
||||
@pytest.mark.parametrize("async_chunk", [False, True])
|
||||
@pytest.mark.parametrize("stop", ["token", "length"])
|
||||
def test_non_resumable_output_stops_before_scheduler_terminal(plane, async_chunk, stop):
|
||||
plane._async_chunk = async_chunk
|
||||
plane._full_payload_replace_keys_cached = frozenset()
|
||||
request = _new_request()
|
||||
request.resumable = False
|
||||
request.sampling_params = SamplingParams(stop_token_ids=[2150], max_tokens=2 if stop == "length" else 10)
|
||||
plane.register_request(request)
|
||||
first, last, stale = (torch.tensor([[value]]) for value in (1, 2, 99))
|
||||
plane.reserve_outputs(["internal"])
|
||||
_complete(plane, [{"codes.audio": first}], token=21)
|
||||
plane.reserve_outputs(["internal"])
|
||||
_complete(plane, [{"codes.audio": last}], token=2150 if stop == "token" else 22)
|
||||
plane.reserve_outputs(["internal"])
|
||||
assert plane.request_terminal({"internal"}) == 0
|
||||
_complete(plane, [{"codes.audio": stale}], token=23)
|
||||
|
||||
payloads = [
|
||||
payload["codes"]["audio"] if async_chunk else payload["codes.audio"]
|
||||
for batch in plane.record.batches
|
||||
for _, payload in batch
|
||||
if payload
|
||||
]
|
||||
torch.testing.assert_close(torch.cat(payloads), torch.cat([first, last]))
|
||||
terminal, _ = plane.record.batches[-1][0]
|
||||
assert terminal.is_finished()
|
||||
assert terminal.output_token_count == 2
|
||||
assert plane.record.cleaned == ["internal"]
|
||||
assert not plane._native_outputs_in_flight and not plane._native_terminal_pending
|
||||
|
||||
|
||||
@pytest.mark.parametrize("ignore_eos", [False, True])
|
||||
def test_native_output_honors_ignore_eos_and_min_tokens(plane, ignore_eos):
|
||||
request = _new_request()
|
||||
request.resumable = False
|
||||
request.sampling_params = SamplingParams(ignore_eos=ignore_eos, min_tokens=2, max_tokens=10)
|
||||
request.sampling_params.update_from_generation_config({}, eos_token_id=99)
|
||||
plane.register_request(request)
|
||||
for token in (99, 99, 21):
|
||||
_complete(plane, [{"codes.audio": torch.tensor([[token]])}], token=token)
|
||||
state = plane._native_requests["internal"]
|
||||
assert state.output_token_ids == ([99, 99, 21] if ignore_eos else [99, 99])
|
||||
assert len(plane.record.batches) == (3 if ignore_eos else 2)
|
||||
|
||||
|
||||
def test_resumable_output_keeps_scheduler_owned_segment_boundary(plane):
|
||||
plane.register_request(_new_request())
|
||||
for token in (21, 2150, 22):
|
||||
_complete(plane, [{"codes.audio": torch.tensor([[token]])}], token=token)
|
||||
assert plane._native_requests["internal"].output_token_ids == [21, 2150, 22]
|
||||
assert len(plane.record.batches) == 3
|
||||
|
||||
|
||||
def test_full_payload_qwen_builder_receives_complete_codec(raw_plane, monkeypatch):
|
||||
from vllm_omni.model_executor.stage_input_processors.qwen3_tts import talker2code2wav_full_payload
|
||||
|
||||
|
||||
@@ -99,9 +99,9 @@ from vllm_omni.model_executor.models.minicpmo_4_5.pipeline import MINICPMO_4_5_P
|
||||
from vllm_omni.model_executor.models.minimax_h3.pipeline import MINIMAX_H3_PIPELINE
|
||||
from vllm_omni.model_executor.models.minimax_music3.pipeline import MINIMAX_MUSIC3_PIPELINE
|
||||
from vllm_omni.model_executor.models.moss_tts.pipeline import (
|
||||
MOSS_TTS_LOCAL_PIPELINE,
|
||||
MOSS_TTS_PIPELINE,
|
||||
MOSS_TTS_REALTIME_PIPELINE,
|
||||
resolve_moss_tts_local_pipeline,
|
||||
)
|
||||
from vllm_omni.model_executor.models.moss_tts_nano.pipeline import MOSS_TTS_NANO_PIPELINE
|
||||
from vllm_omni.model_executor.models.nemotron_voicechat.pipeline import (
|
||||
@@ -183,7 +183,7 @@ OMNI_PIPELINES: dict[str, PipelineConfig | PipelineResolverFunc] = {
|
||||
"minimax_h3_disaggregated": MINIMAX_H3_PIPELINE,
|
||||
"minimax_music3": MINIMAX_MUSIC3_PIPELINE,
|
||||
"moss_tts_delay": MOSS_TTS_PIPELINE,
|
||||
"moss_tts_local": MOSS_TTS_LOCAL_PIPELINE,
|
||||
"moss_tts_local": resolve_moss_tts_local_pipeline,
|
||||
"moss_tts_nano": MOSS_TTS_NANO_PIPELINE,
|
||||
"moss_tts_realtime": MOSS_TTS_REALTIME_PIPELINE,
|
||||
# Alias: the Nemotron-Labs-Audex-2B repo-root config.json reports
|
||||
|
||||
@@ -914,6 +914,11 @@ def _apply_platform_overrides(
|
||||
device_name = current_omni_platform.device_name
|
||||
platform = device_name.lower() if device_name is not None else None
|
||||
platform_section = (deploy.platforms or {}).get(platform) if platform is not None else None
|
||||
if platform_section is not None and "cuda_mps" in platform_section:
|
||||
cuda_mps = platform_section["cuda_mps"]
|
||||
if not isinstance(cuda_mps, bool):
|
||||
raise ValueError("platform cuda_mps must be a boolean")
|
||||
deploy.cuda_mps = cuda_mps
|
||||
if platform_section is not None and "model_runner" in platform_section:
|
||||
model_runner = platform_section["model_runner"]
|
||||
if model_runner not in ("v1", "v2"):
|
||||
|
||||
@@ -1,88 +1,9 @@
|
||||
# MOSS-TTS-Local-Transformer-v1.5 (MossTTSLocalModel, n_vq=12, 48 kHz stereo)
|
||||
# HF: OpenMOSS-Team/MOSS-TTS-Local-Transformer-v1.5
|
||||
# Codec: OpenMOSS-Team/MOSS-Audio-Tokenizer-v2 (48 kHz, stereo, channel-interleaved RVQ)
|
||||
# Uses a 1-layer GPT2-style local depth transformer (no delay warm-up), reset every
|
||||
# frame — same per-frame timing (80 ms/frame, 12.5 fps) as MOSS-TTS-Realtime.
|
||||
async_chunk: true
|
||||
# CUDA MRV2/C64 default with full-quota private MPS.
|
||||
# Automatic selection uses C128 on >=140 GiB CUDA GPUs, or this C64 profile
|
||||
# on smaller/unknown-memory GPUs. Missing MPS selects moss_tts_local_mrv2.yaml.
|
||||
# Non-CUDA platforms retain V1. Explicit deploy configs override selection.
|
||||
base_config: moss_tts_local_mrv2.yaml
|
||||
|
||||
speech_cache:
|
||||
resolve_max_bytes: 2147483648 # 2 GiB
|
||||
resolve_max_entries: 2048
|
||||
|
||||
connectors:
|
||||
shm:
|
||||
name: SharedMemoryConnector
|
||||
extra:
|
||||
connector_get_sleep_s: 0.005
|
||||
connector_get_max_wait_first_chunk: 1000
|
||||
connector_get_max_wait: 300
|
||||
initial_codec_chunk_frames: 1 # 1 frame * 80 ms/frame = 80 ms first audio chunk
|
||||
codec_chunk_frames: 15 # 15 frames * 80 ms/frame = 1.2 s steady chunks
|
||||
# Optional startup ramp; its first entry overrides initial chunk size.
|
||||
# Extra graph shapes/calls trade codec cost and latency for less underrun.
|
||||
# codec_chunk_ramp: [1, 4, 15]
|
||||
codec_left_context_frames: 0
|
||||
# Chunk ramp (opt-in): gradual chunk-size ladder replacing the
|
||||
# initial_codec_chunk_frames -> codec_chunk_frames cliff. The last
|
||||
# entry should equal codec_chunk_frames (steady state).
|
||||
# codec_chunk_ramp: [2, 4, 8, 15]
|
||||
|
||||
stages:
|
||||
- stage_id: 0
|
||||
max_num_seqs: 64
|
||||
gpu_memory_utilization: 0.60
|
||||
enable_prefix_caching: false
|
||||
async_scheduling: true
|
||||
max_num_batched_tokens: 65536
|
||||
max_model_len: 4096
|
||||
devices: "0"
|
||||
# profiler_config:
|
||||
# profiler: torch
|
||||
# torch_profiler_dir: /root/vllm-omni-workspace/vllm-omni/results/moss_local_stage0/batch_8
|
||||
output_connectors:
|
||||
to_stage_1: shm
|
||||
default_sampling_params:
|
||||
temperature: 1.7
|
||||
top_p: 0.8
|
||||
top_k: 25
|
||||
max_tokens: 4096
|
||||
seed: 42
|
||||
repetition_penalty: 1.0
|
||||
|
||||
- stage_id: 1
|
||||
max_num_seqs: 64
|
||||
gpu_memory_utilization: 0.30
|
||||
enforce_eager: false
|
||||
enable_prefix_caching: false
|
||||
async_scheduling: true
|
||||
compilation_config:
|
||||
# Reused by the codec as execution-batch buckets; T is derived from
|
||||
# initial_codec_chunk_frames and codec_chunk_frames.
|
||||
cudagraph_capture_sizes: [1, 2, 4, 8, 16, 32, 64]
|
||||
cudagraph_num_of_warmups: 1
|
||||
# profiler_config:
|
||||
# profiler: torch
|
||||
# torch_profiler_dir: /root/vllm-omni-workspace/vllm-omni/results/moss_local_stage1/batch_8
|
||||
max_num_batched_tokens: 65536
|
||||
max_model_len: 65536
|
||||
devices: "0"
|
||||
input_connectors:
|
||||
from_stage_0: shm
|
||||
default_sampling_params:
|
||||
temperature: 0.0
|
||||
top_p: 1.0
|
||||
top_k: -1
|
||||
max_tokens: 65536
|
||||
seed: 42
|
||||
repetition_penalty: 1.0
|
||||
|
||||
# Stage 1 NPU override: PIECEWISE cudagraph for NPUGraph capture; set enforce_eager: true to disable.
|
||||
platforms:
|
||||
npu:
|
||||
stages:
|
||||
- stage_id: 1
|
||||
compilation_config:
|
||||
cudagraph_mode: PIECEWISE
|
||||
# Re-declared: _apply_platform_overrides() wholesale-replaces compilation_config.
|
||||
cudagraph_capture_sizes: [1, 2, 4, 8, 16, 32, 64]
|
||||
cudagraph_num_of_warmups: 1
|
||||
cuda:
|
||||
cuda_mps: true
|
||||
@@ -1,10 +1,11 @@
|
||||
# Opt-in CUDA MRV2 for MOSS-TTS-Local-Transformer-v1.5.
|
||||
# Keep the base profile's batching, chunk geometry and V1 default.
|
||||
base_config: moss_tts_local.yaml
|
||||
model_runner: v2
|
||||
# CUDA MRV2/C64 for MOSS-TTS-Local-Transformer-v1.5, without MPS.
|
||||
# Also selected automatically when nvidia-cuda-mps-control is unavailable.
|
||||
base_config: moss_tts_local_v1.yaml
|
||||
model_runner: v1
|
||||
|
||||
platforms:
|
||||
cuda:
|
||||
model_runner: v2
|
||||
stages:
|
||||
- stage_id: 0
|
||||
# Bound prefill work so new prompts do not monopolize streaming decode.
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
# Automatic CUDA MRV2/C128 default on GPUs with at least 140 GiB total memory
|
||||
# when nvidia-cuda-mps-control is available. Keep the published C128 baseline
|
||||
# and its original Local projection/sampler; add full-quota private MPS only.
|
||||
base_config: moss_tts_local_mrv2_high_concurrency.yaml
|
||||
|
||||
platforms:
|
||||
cuda:
|
||||
cuda_mps: true
|
||||
@@ -0,0 +1,43 @@
|
||||
# CUDA system profile for MOSS-TTS-Local-Transformer-v1.5.
|
||||
# Selected automatically with at least 140 GiB GPU memory and MPS available.
|
||||
# Requires H200-class memory and nvidia-cuda-mps-control on PATH.
|
||||
# Adds prefix caching, Triton backbone attention and faster codec dispatch;
|
||||
# keeps the original Local depth projection and native Torch sampler.
|
||||
# The throughput baseline remains moss_tts_local_mrv2_high_concurrency.yaml.
|
||||
base_config: moss_tts_local_mrv2_high_concurrency.yaml
|
||||
model_runner: v1
|
||||
|
||||
connectors:
|
||||
shm:
|
||||
extra:
|
||||
codec_first_chunk_fast_path: 1
|
||||
# Keep the fast first chunk for low load; coalesce crowded streams.
|
||||
codec_first_chunk_max_active_streams: 32
|
||||
generation_min_batch_size: 16
|
||||
generation_max_wait_ms: 6
|
||||
|
||||
platforms:
|
||||
cuda:
|
||||
model_runner: v2
|
||||
cuda_mps: true
|
||||
stages:
|
||||
- stage_id: 0
|
||||
hf_overrides:
|
||||
mrv2_gpu_slot_state: true
|
||||
mrv2_batch_prefill: true
|
||||
mrv2_direct_tokens: true
|
||||
# Compile the native Torch sampler; explicit generators keep its fallback.
|
||||
local_compile_audio_sampler: true
|
||||
enable_prefix_caching: true
|
||||
attention_config:
|
||||
backend: TRITON_ATTN
|
||||
- stage_id: 1
|
||||
compilation_config:
|
||||
mode: 3
|
||||
backend: inductor
|
||||
cudagraph_mode: FULL
|
||||
cudagraph_capture_sizes: [1, 2, 3, 4, 5, 6, 7, 8, 10, 12, 14, 16, 20, 24, 28, 32, 40, 48, 56, 64, 80, 96, 112, 128]
|
||||
cudagraph_num_of_warmups: 1
|
||||
inductor_compile_config:
|
||||
combo_kernels: false
|
||||
benchmark_combo_kernel: false
|
||||
@@ -0,0 +1,88 @@
|
||||
# MOSS-TTS-Local-Transformer-v1.5 (MossTTSLocalModel, n_vq=12, 48 kHz stereo)
|
||||
# HF: OpenMOSS-Team/MOSS-TTS-Local-Transformer-v1.5
|
||||
# Codec: OpenMOSS-Team/MOSS-Audio-Tokenizer-v2 (48 kHz, stereo, channel-interleaved RVQ)
|
||||
# Uses a 1-layer GPT2-style local depth transformer (no delay warm-up), reset every
|
||||
# frame — same per-frame timing (80 ms/frame, 12.5 fps) as MOSS-TTS-Realtime.
|
||||
async_chunk: true
|
||||
|
||||
speech_cache:
|
||||
resolve_max_bytes: 2147483648 # 2 GiB
|
||||
resolve_max_entries: 2048
|
||||
|
||||
connectors:
|
||||
shm:
|
||||
name: SharedMemoryConnector
|
||||
extra:
|
||||
connector_get_sleep_s: 0.005
|
||||
connector_get_max_wait_first_chunk: 1000
|
||||
connector_get_max_wait: 300
|
||||
initial_codec_chunk_frames: 1 # 1 frame * 80 ms/frame = 80 ms first audio chunk
|
||||
codec_chunk_frames: 15 # 15 frames * 80 ms/frame = 1.2 s steady chunks
|
||||
# Optional startup ramp; its first entry overrides initial chunk size.
|
||||
# Extra graph shapes/calls trade codec cost and latency for less underrun.
|
||||
# codec_chunk_ramp: [1, 4, 15]
|
||||
codec_left_context_frames: 0
|
||||
# Chunk ramp (opt-in): gradual chunk-size ladder replacing the
|
||||
# initial_codec_chunk_frames -> codec_chunk_frames cliff. The last
|
||||
# entry should equal codec_chunk_frames (steady state).
|
||||
# codec_chunk_ramp: [2, 4, 8, 15]
|
||||
|
||||
stages:
|
||||
- stage_id: 0
|
||||
max_num_seqs: 64
|
||||
gpu_memory_utilization: 0.60
|
||||
enable_prefix_caching: false
|
||||
async_scheduling: true
|
||||
max_num_batched_tokens: 65536
|
||||
max_model_len: 4096
|
||||
devices: "0"
|
||||
# profiler_config:
|
||||
# profiler: torch
|
||||
# torch_profiler_dir: /root/vllm-omni-workspace/vllm-omni/results/moss_local_stage0/batch_8
|
||||
output_connectors:
|
||||
to_stage_1: shm
|
||||
default_sampling_params:
|
||||
temperature: 1.7
|
||||
top_p: 0.8
|
||||
top_k: 25
|
||||
max_tokens: 4096
|
||||
seed: 42
|
||||
repetition_penalty: 1.0
|
||||
|
||||
- stage_id: 1
|
||||
max_num_seqs: 64
|
||||
gpu_memory_utilization: 0.30
|
||||
enforce_eager: false
|
||||
enable_prefix_caching: false
|
||||
async_scheduling: true
|
||||
compilation_config:
|
||||
# Reused by the codec as execution-batch buckets; T is derived from
|
||||
# initial_codec_chunk_frames and codec_chunk_frames.
|
||||
cudagraph_capture_sizes: [1, 2, 4, 8, 16, 32, 64]
|
||||
cudagraph_num_of_warmups: 1
|
||||
# profiler_config:
|
||||
# profiler: torch
|
||||
# torch_profiler_dir: /root/vllm-omni-workspace/vllm-omni/results/moss_local_stage1/batch_8
|
||||
max_num_batched_tokens: 65536
|
||||
max_model_len: 65536
|
||||
devices: "0"
|
||||
input_connectors:
|
||||
from_stage_0: shm
|
||||
default_sampling_params:
|
||||
temperature: 0.0
|
||||
top_p: 1.0
|
||||
top_k: -1
|
||||
max_tokens: 65536
|
||||
seed: 42
|
||||
repetition_penalty: 1.0
|
||||
|
||||
# Stage 1 NPU override: PIECEWISE cudagraph for NPUGraph capture; set enforce_eager: true to disable.
|
||||
platforms:
|
||||
npu:
|
||||
stages:
|
||||
- stage_id: 1
|
||||
compilation_config:
|
||||
cudagraph_mode: PIECEWISE
|
||||
# Re-declared: _apply_platform_overrides() wholesale-replaces compilation_config.
|
||||
cudagraph_capture_sizes: [1, 2, 4, 8, 16, 32, 64]
|
||||
cudagraph_num_of_warmups: 1
|
||||
@@ -1698,7 +1698,7 @@ class OmniOpenAIServingSpeech(OpenAIServing, AudioMixin):
|
||||
chunk_np = resampler.process(chunk_np)
|
||||
if chunk_np.size == 0:
|
||||
continue
|
||||
if self._tts_model_type in _AUDEX_NO_AUDIO_GUARD_MODEL_TYPES and int(np.size(chunk_np)) == 0:
|
||||
if int(np.size(chunk_np)) == 0:
|
||||
# Zero-size chunks must not emit a WAV header or count
|
||||
# as first audio; the post-loop guard below needs to
|
||||
# see an audio-less stream to fail the request.
|
||||
@@ -1762,6 +1762,8 @@ class OmniOpenAIServingSpeech(OpenAIServing, AudioMixin):
|
||||
# Audex contract: zero codec tokens must abort the stream, not
|
||||
# complete it cleanly with zero audio bytes.
|
||||
raise ValueError("Audex produced no audio (the thinker emitted zero or invalid codec tokens)")
|
||||
if adapter is not None:
|
||||
adapter.validate_stream_audio(has_audio=first_audio_chunk_s is not None)
|
||||
# Check before committing the reference-audio artifact or logging
|
||||
# success. Streaming protocols may already have emitted partial
|
||||
# bytes, but they must terminate as an error rather than cleanly.
|
||||
|
||||
@@ -331,6 +331,9 @@ class TTSModelAdapter(ABC):
|
||||
to unrelated TTS models.
|
||||
"""
|
||||
|
||||
def validate_stream_audio(self, *, has_audio: bool) -> None:
|
||||
"""Validate audio availability before recording streaming success."""
|
||||
|
||||
def collect_response_metadata(self, audio_output: Mapping[str, Any], collect: dict) -> None:
|
||||
"""Fold engine-side metadata from the mm payload into ``collect``.
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ from vllm_omni.entrypoints.openai.tts_adapters.base import (
|
||||
ARTTSAdapter,
|
||||
OutputPolicy,
|
||||
PreparedRequest,
|
||||
TTSGenerationError,
|
||||
apply_max_new_tokens,
|
||||
conditioning_cache_salt,
|
||||
)
|
||||
@@ -519,6 +520,11 @@ class _MossTTSAdapterBase(ARTTSAdapter):
|
||||
params["ref_audio_2_cache_key"] = resolve_keys[1]
|
||||
return params
|
||||
|
||||
def validate_stream_audio(self, *, has_audio: bool) -> None:
|
||||
# Local's sampled binary gate can stop before its first codec frame.
|
||||
if self._moss_variant == "local" and not has_audio:
|
||||
raise TTSGenerationError("MOSS-TTS Local produced no audio output.")
|
||||
|
||||
def validate(self, request: "OpenAICreateSpeechRequest") -> str | None:
|
||||
"""Validate any MOSS-TTS-family request (nano + 5 full variants).
|
||||
|
||||
|
||||
@@ -64,6 +64,7 @@ class MossFirstChunkFastPath:
|
||||
sample_rate: torch.Tensor,
|
||||
device: torch.device,
|
||||
gate_main: bool = False,
|
||||
max_active_streams: int = 0,
|
||||
handoff_timeout_s: float = 30.0,
|
||||
) -> None:
|
||||
if not math.isfinite(handoff_timeout_s) or handoff_timeout_s <= 0:
|
||||
@@ -74,6 +75,7 @@ class MossFirstChunkFastPath:
|
||||
# before its next replay, so the small first-chunk graph runs mostly
|
||||
# uncontended instead of interleaving with a large regular replay.
|
||||
self._gate_main = bool(gate_main)
|
||||
self._max_active_streams = max(0, int(max_active_streams))
|
||||
self._inflight: torch.cuda.Event | None = None
|
||||
self._wrapper = wrapper
|
||||
self._n_vq = int(n_vq)
|
||||
@@ -154,6 +156,7 @@ class MossFirstChunkFastPath:
|
||||
or self._thread is None
|
||||
or not self._thread.is_alive()
|
||||
or request_key in req_slots
|
||||
or (self._max_active_streams and len(req_slots) >= self._max_active_streams)
|
||||
):
|
||||
return False
|
||||
delivery = self._sink.prepare([request_id])
|
||||
|
||||
@@ -208,9 +208,12 @@ class MossLocalModelState(OmniModelState):
|
||||
|
||||
def _select_rows(self, tensor, rows):
|
||||
if rows == list(range(len(rows))):
|
||||
return tensor[: len(rows)]
|
||||
selected = tensor[: len(rows)]
|
||||
else:
|
||||
indices = _metadata_to_device(np.asarray(rows, dtype=np.int64), tensor.device)
|
||||
return tensor.index_select(0, indices)
|
||||
selected = tensor.index_select(0, indices)
|
||||
# vLLM 0.31 uses int32 slot mappings; index_copy_ requires int64.
|
||||
return selected.long()
|
||||
|
||||
def run_preprocess(self, input_batch, model_inputs, req_states=None, mtp_batch_descriptor_dispatcher=None):
|
||||
input_ids = model_inputs.get("input_ids")
|
||||
@@ -255,7 +258,9 @@ class MossLocalModelState(OmniModelState):
|
||||
continue
|
||||
if self._batch_prefill:
|
||||
ref = (buf.get("codes", {}) or {}).get("ref")
|
||||
ref_offset = int(buf.get("ref_offset", 0))
|
||||
# Prefix hits skip hooks for cached prompt tokens. Use the
|
||||
# scheduler's absolute prompt position, also on first admission.
|
||||
ref_offset = int(computed) if computed is not None else int(buf.get("ref_offset", 0))
|
||||
# Device-side references keep the canonical path; do not add
|
||||
# an implicit D2H just to place them in a host staging buffer.
|
||||
if not isinstance(ref, torch.Tensor) or ref.device.type == "cpu":
|
||||
@@ -264,7 +269,7 @@ class MossLocalModelState(OmniModelState):
|
||||
ref = ref.view(-1, self.model.n_vq)
|
||||
if ref.dim() == 2:
|
||||
chunk = ref[ref_offset : ref_offset + count]
|
||||
if chunk.numel() and chunk.shape[0] == count:
|
||||
if chunk.numel():
|
||||
references.append((start, chunk))
|
||||
# Text embeddings have already been computed for all input
|
||||
# tokens. Match the canonical prefill state transition.
|
||||
@@ -419,7 +424,7 @@ class MossLocalModelState(OmniModelState):
|
||||
# mixed/chunked-prefill batches. index_copy snapshots graph outputs.
|
||||
last = input_batch.query_start_loc[1 : input_batch.num_reqs + 1] - 1
|
||||
self._hidden_pool.index_copy_(
|
||||
0, input_batch.idx_mapping[: input_batch.num_reqs], hidden_states.index_select(0, last)
|
||||
0, input_batch.idx_mapping[: input_batch.num_reqs].long(), hidden_states.index_select(0, last)
|
||||
)
|
||||
if self._local_eager_mtp:
|
||||
completing = self._completing_rows
|
||||
|
||||
@@ -802,6 +802,7 @@ class MossTTSCodecDecoder(nn.Module):
|
||||
sample_rate=self._sr_tensor,
|
||||
device=next(self._codec.parameters()).device,
|
||||
gate_main=bool(self._connector_int("codec_first_chunk_gate", default=0)),
|
||||
max_active_streams=self._connector_int("codec_first_chunk_max_active_streams", default=0),
|
||||
)
|
||||
logger.info(
|
||||
"MOSS-TTS codec first-chunk fast path ready: B=%s T=%d",
|
||||
|
||||
@@ -197,7 +197,9 @@ class MossTTSLocalDepthTransformer(nn.Module):
|
||||
every cache position before reading it, including CUDA Graph replay.
|
||||
"""
|
||||
|
||||
def __init__(self, gpt2_config, hidden_size: int | None = None) -> None:
|
||||
def __init__(
|
||||
self, gpt2_config, hidden_size: int | None = None, *, compile_audio_sampler: bool | None = None
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.hidden_size = int(hidden_size if hidden_size is not None else gpt2_config.n_embd)
|
||||
n_head = int(gpt2_config.n_head)
|
||||
@@ -208,6 +210,7 @@ class MossTTSLocalDepthTransformer(nn.Module):
|
||||
self.ln_f = nn.LayerNorm(self.hidden_size, eps=eps)
|
||||
self._compiled_forward_prefix = None
|
||||
self._compiled_audio_sampler = None
|
||||
self._compile_audio_sampler = compile_audio_sampler
|
||||
|
||||
def _forward_prefix(
|
||||
self,
|
||||
@@ -231,7 +234,10 @@ class MossTTSLocalDepthTransformer(nn.Module):
|
||||
dynamic=True,
|
||||
options={"epilogue_fusion": False},
|
||||
)
|
||||
if os.environ.get("VLLM_OMNI_MOSS_LOCAL_COMPILE_AUDIO_SAMPLER", "0") == "1":
|
||||
compile_sampler = self._compile_audio_sampler
|
||||
if compile_sampler is None:
|
||||
compile_sampler = os.environ.get("VLLM_OMNI_MOSS_LOCAL_COMPILE_AUDIO_SAMPLER", "0") == "1"
|
||||
if compile_sampler:
|
||||
# Keep the existing top-k/top-p algorithm and torch RNG. Explicit
|
||||
# per-request generators use the original helper below. The
|
||||
# binary continue/stop head is deliberately unchanged.
|
||||
|
||||
@@ -1349,7 +1349,11 @@ class MossTTSLocalTalkerForGeneration(nn.Module):
|
||||
[nn.Linear(hidden_size, self.audio_vocab_size, bias=False) for _ in range(self.n_vq)]
|
||||
)
|
||||
self.local_text_lm_head = nn.Linear(hidden_size, 2, bias=False)
|
||||
self.local_transformer = MossTTSLocalDepthTransformer(self.config.gpt2_config, hidden_size=hidden_size)
|
||||
self.local_transformer = MossTTSLocalDepthTransformer(
|
||||
self.config.gpt2_config,
|
||||
hidden_size=hidden_size,
|
||||
compile_audio_sampler=getattr(self.config, "local_compile_audio_sampler", None),
|
||||
)
|
||||
|
||||
self._batch_state: list[dict[str, Any]] | None = None
|
||||
# Stacked embedding cache built after load_weights() for vectorised
|
||||
@@ -1520,16 +1524,18 @@ class MossTTSLocalTalkerForGeneration(nn.Module):
|
||||
embeds = self.model.embed_tokens(input_ids)
|
||||
|
||||
ref_codes = (info_dict.get("codes", {}) or {}).get("ref")
|
||||
ref_offset = int(info_dict.get("ref_offset", 0))
|
||||
# Cached prompt tokens already consumed their reference rows even
|
||||
# though this request's preprocessing hook never saw those tokens.
|
||||
ref_offset = int(info_dict.get("_omni_num_computed_tokens", info_dict.get("ref_offset", 0)))
|
||||
if isinstance(ref_codes, torch.Tensor) and ref_codes.numel() > 0:
|
||||
if ref_codes.dim() == 1 and ref_codes.numel() % self.n_vq == 0:
|
||||
ref_codes = ref_codes.view(-1, self.n_vq)
|
||||
if isinstance(ref_codes, torch.Tensor) and ref_codes.dim() == 2:
|
||||
end_off = ref_offset + span_len
|
||||
chunk = ref_codes[ref_offset:end_off]
|
||||
if chunk.numel() > 0 and chunk.shape[0] == span_len:
|
||||
if chunk.numel() > 0:
|
||||
codes = chunk.to(device=device, dtype=torch.long)
|
||||
embeds = embeds + self._audio_embed(codes)
|
||||
embeds[: chunk.shape[0]] += self._audio_embed(codes)
|
||||
|
||||
info_update: dict[str, Any] = {
|
||||
"audio_state": {"is_stopping": False},
|
||||
@@ -1701,6 +1707,10 @@ class MossTTSLocalTalkerForGeneration(nn.Module):
|
||||
"""Honor the same platform graph-safety override as the V1 runner."""
|
||||
return self.talker_mtp_graph_safe
|
||||
|
||||
@property
|
||||
def mtp_accepts_per_row_generators(self) -> bool:
|
||||
return self.talker_mtp_accepts_per_row_generators
|
||||
|
||||
# Package runner-generated audio frames
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
|
||||
@@ -6,6 +6,11 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License").
|
||||
"""Pipeline topology for all MOSS-TTS variants (2-stage: talker → codec)."""
|
||||
|
||||
import shutil
|
||||
from dataclasses import replace
|
||||
|
||||
from transformers import PretrainedConfig
|
||||
from vllm.logger import init_logger
|
||||
from vllm.sampling_params import RequestOutputKind
|
||||
|
||||
from vllm_omni.config.stage_config import (
|
||||
@@ -13,8 +18,10 @@ from vllm_omni.config.stage_config import (
|
||||
StageExecutionType,
|
||||
StagePipelineConfig,
|
||||
)
|
||||
from vllm_omni.model_executor.models.moss_tts.configuration_moss_tts import MossTTSLocalConfig
|
||||
|
||||
_PROC = "vllm_omni.model_executor.stage_input_processors.moss_tts"
|
||||
logger = init_logger(__name__)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Shared 2-stage pipeline (used by all 5 MOSS-TTS variants)
|
||||
@@ -135,4 +142,40 @@ MOSS_TTS_LOCAL_PIPELINE = PipelineConfig(
|
||||
# they have different talker architectures from the delay variant
|
||||
# (MossTTSRealtime / MossTTSLocalModel vs MossTTSDelayModel).
|
||||
|
||||
__all__ = ["MOSS_TTS_PIPELINE", "MOSS_TTS_REALTIME_PIPELINE", "MOSS_TTS_LOCAL_PIPELINE"]
|
||||
|
||||
def resolve_moss_tts_local_pipeline(hf_config: PretrainedConfig | None = None) -> PipelineConfig | None:
|
||||
"""Select CUDA MRV2, with the C128 system profile when prerequisites are met.
|
||||
|
||||
The platform memory query uses NVML, avoiding CUDA initialization before
|
||||
workers spawn. Explicit deploy configs override this pipeline default.
|
||||
"""
|
||||
from vllm.platforms import current_platform
|
||||
|
||||
from vllm_omni.platforms import current_omni_platform
|
||||
|
||||
if hf_config is not None and not isinstance(hf_config, MossTTSLocalConfig):
|
||||
return None
|
||||
if current_omni_platform.device_name != "cuda":
|
||||
return replace(MOSS_TTS_LOCAL_PIPELINE, default_deploy_config_name="moss_tts_local_v1.yaml")
|
||||
if shutil.which("nvidia-cuda-mps-control") is None:
|
||||
logger.info("MOSS Local defaults to CUDA MRV2/C64 without MPS: nvidia-cuda-mps-control is unavailable")
|
||||
return replace(MOSS_TTS_LOCAL_PIPELINE, default_deploy_config_name="moss_tts_local_mrv2.yaml")
|
||||
try:
|
||||
memory_bytes = current_platform.get_device_total_memory()
|
||||
except Exception as exc:
|
||||
logger.warning("Cannot query CUDA memory for MOSS Local; using MRV2/C64 with MPS: %s", exc)
|
||||
return MOSS_TTS_LOCAL_PIPELINE
|
||||
if memory_bytes >= 140 * 1024**3:
|
||||
return replace(
|
||||
MOSS_TTS_LOCAL_PIPELINE,
|
||||
default_deploy_config_name="moss_tts_local_mrv2_optimized.yaml",
|
||||
)
|
||||
return MOSS_TTS_LOCAL_PIPELINE
|
||||
|
||||
|
||||
__all__ = [
|
||||
"MOSS_TTS_PIPELINE",
|
||||
"MOSS_TTS_REALTIME_PIPELINE",
|
||||
"MOSS_TTS_LOCAL_PIPELINE",
|
||||
"resolve_moss_tts_local_pipeline",
|
||||
]
|
||||
@@ -45,6 +45,7 @@ from vllm.triton_utils import tl, triton
|
||||
from vllm_omni.platforms import current_omni_platform
|
||||
|
||||
_KERNEL = 7
|
||||
_CUDA_SIN = current_omni_platform.is_cuda()
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- kernels
|
||||
@@ -68,13 +69,15 @@ def _rvq_gather_kernel(codes_ptr, books_ptr, out_ptr, NQ: tl.constexpr, CB: tl.c
|
||||
|
||||
|
||||
@triton.jit
|
||||
def _fast_sin(y):
|
||||
def _fast_sin(y, CUDA_SIN: tl.constexpr = _CUDA_SIN):
|
||||
# Two-step reduction to [-pi, pi], then the hardware approximation
|
||||
# (absolute error ~4e-7 there): accurate sin's instruction count, not
|
||||
# memory, bounds every kernel that applies SnakeBeta.
|
||||
k = tl.extra.cuda.libdevice.rint(y * 0.15915494309189535)
|
||||
k = tl.extra.libdevice.rint(y * 0.15915494309189535)
|
||||
r = tl.fma(-k, -1.7484555314695172e-07, tl.fma(-k, 6.2831854820251465, y))
|
||||
if CUDA_SIN:
|
||||
return tl.inline_asm_elementwise("sin.approx.f32 $0, $1;", "=r,r", [r], dtype=tl.float32, is_pure=True, pack=1)
|
||||
return tl.sin(r)
|
||||
|
||||
|
||||
@triton.jit
|
||||
|
||||
@@ -38,6 +38,30 @@ class _NativeRequestState:
|
||||
resumable: bool = False
|
||||
output_token_ids: list[int] = field(default_factory=list)
|
||||
finished: bool = False
|
||||
output_stopped: bool = False
|
||||
|
||||
def accept_tokens(self, token_ids: list[int]) -> None:
|
||||
"""Fence publications past the sampled stop, before scheduler ACK.
|
||||
|
||||
Async scheduling may already have executed another step when the
|
||||
scheduler consumes EOS. Keep the terminating step's payload, but do
|
||||
not publish the later steps while lifecycle reservations drain.
|
||||
Resumable requests keep their segment boundaries owned by the scheduler.
|
||||
"""
|
||||
params = self.sampling_params
|
||||
stop_ids = set(getattr(params, "stop_token_ids", None) or ())
|
||||
if not getattr(params, "ignore_eos", False):
|
||||
stop_ids.update(getattr(params, "_all_stop_token_ids", ()) or ())
|
||||
min_tokens = getattr(params, "min_tokens", 0) or 0
|
||||
max_tokens = getattr(params, "max_tokens", None)
|
||||
for token_id in token_ids:
|
||||
self.output_token_ids.append(int(token_id))
|
||||
if not self.resumable and (
|
||||
(len(self.output_token_ids) >= min_tokens and token_id in stop_ids)
|
||||
or (max_tokens is not None and len(self.output_token_ids) >= max_tokens)
|
||||
):
|
||||
self.output_stopped = True
|
||||
break
|
||||
|
||||
def snapshot(self, *, include_token_history: bool) -> SimpleNamespace:
|
||||
prompt = list(self.prompt_token_ids) if include_token_history else []
|
||||
@@ -506,8 +530,14 @@ class OmniRunnerDataPlane(OmniConnectorModelRunnerMixin):
|
||||
state = self._native_requests.get(req_id)
|
||||
if state is None:
|
||||
continue
|
||||
state.output_token_ids.extend(int(token_id) for token_id in sampled_by_req.get(req_id, []))
|
||||
state.finished = req_id in terminal_req_ids
|
||||
# Native transport runs ahead of scheduler consumption. Its
|
||||
# terminal notification fences resource cleanup, not the sampled
|
||||
# token boundary: a later in-flight frame must not reach the codec.
|
||||
if state.output_stopped:
|
||||
payload = None
|
||||
else:
|
||||
state.accept_tokens(sampled_by_req.get(req_id, []))
|
||||
payload = payload_by_req.get(req_id)
|
||||
if not self._async_chunk:
|
||||
# Full-payload consumers execute once, after the producer has
|
||||
|
||||
Reference in new issue
Block a user