[Bugfix] Avoid waveform-list GC scans and release completed MOSS batches (#7885)

Signed-off-by: Canlin Guo <canlinguosdu@gmail.com>
This commit is contained in:
Canlin Guo authored and GitHub committed 2026-09-20 05:36:52 -07:00
1 parent 23f4126445
commit 4d877780d3
7 files changed
+238 -53

No files matched your search

@@ -0,0 +1,105 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
import numpy as np
import pytest
from vllm_omni.entrypoints.openai.serving_speech import OmniOpenAIServingSpeech
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
@pytest.fixture
def server(monkeypatch, tmp_path):
monkeypatch.setenv("SPEAKER_SAMPLES_DIR", str(tmp_path))
return OmniOpenAIServingSpeech.__new__(OmniOpenAIServingSpeech)
@pytest.mark.parametrize("stereo", [False, True])
def test_finalized_waveform_owns_contiguous_numeric_storage(server, stereo):
import gc
source = np.linspace(-1, 1, 96000, dtype=np.float64).reshape(48000, 2)
decoded = source if stereo else source[:, 0]
expected = np.asarray(decoded, dtype=np.float32)
if stereo:
expected = expected.mean(axis=-1)
waveform, sr, artifact, duration = server._finalize_fetched_ref_audio(decoded, 24000)
assert waveform.dtype == np.float32
assert waveform.flags.c_contiguous and waveform.flags.owndata
assert not np.shares_memory(waveform, decoded)
assert not gc.is_tracked(waveform)
np.testing.assert_array_equal(waveform, expected)
assert artifact == server._make_ref_audio_artifact_cache_key(expected, sr)
assert duration == 2.0
@pytest.mark.asyncio
async def test_array_resolver_reuses_buffer_and_legacy_lists_are_temporary(server, mocker):
server._init_speaker_storage()
server._diffusion_mode = True
server._allowed_local_media_path = ""
server._ref_audio_cache_key = mocker.AsyncMock(return_value="locator")
server._media_connector = mocker.Mock()
server._media_connector.fetch_audio_async = mocker.AsyncMock(
return_value=(np.linspace(-1, 1, 24000, dtype=np.float32), 24000)
)
first, sr, key = await server._resolve_ref_audio_array("reference")
second, _, _ = await server._resolve_ref_audio_array("reference")
legacy, _, _ = await server._resolve_ref_audio("reference")
assert first is second
assert isinstance(legacy, list)
np.testing.assert_array_equal(first, legacy)
legacy[0] = 100
assert first[0] == -1
assert server._ref_audio_resolve_cache[key][0] is first
assert server._ref_audio_resolve_cache_bytes == first.nbytes == 96000
server._media_connector.fetch_audio_async.assert_awaited_once()
@pytest.mark.parametrize("variant", [None, "local", "delay", "realtime"])
@pytest.mark.asyncio
async def test_moss_array_resolution_keeps_nano_list_transport(mocker, variant):
from vllm_omni.entrypoints.openai.tts_adapters.base import SpeechServingContext
from vllm_omni.entrypoints.openai.tts_adapters.moss_tts import _MossTTSAdapterBase
server = mocker.Mock()
server._resolve_ref_audio = mocker.AsyncMock(return_value=([0.0], 24000, "key"))
waveform = np.zeros(1, dtype=np.float32)
server._resolve_ref_audio_array = mocker.AsyncMock(return_value=(waveform, 24000, "key"))
adapter = _MossTTSAdapterBase.__new__(_MossTTSAdapterBase)
adapter.ctx = SpeechServingContext(server=server)
adapter._moss_variant = variant
result, _, _ = await adapter._resolve_ref_audio("reference")
if variant is None:
assert isinstance(result, list)
server._resolve_ref_audio_array.assert_not_awaited()
else:
assert result is waveform
server._resolve_ref_audio.assert_not_awaited()
def test_waveform_cache_eviction_releases_owned_buffer(server):
import weakref
server._init_speaker_storage()
server._ref_audio_resolve_cache_max_entries = 1
waveform = np.zeros(24000, dtype=np.float32)
ref = weakref.ref(waveform)
server._put_resolved_ref_audio("first", waveform, 24000, "artifact-first")
del waveform
assert ref() is not None
server._put_resolved_ref_audio("second", np.zeros(24000, dtype=np.float32), 24000, "artifact-second")
assert ref() is None
assert server._ref_audio_resolve_cache_bytes == 96000
@pytest.mark.parametrize("entries, budget", [(1, 1000), (10, 8)])
def test_numeric_waveform_cache_limits(server, entries, budget):
server._init_speaker_storage()
server._ref_audio_resolve_cache_max_entries = entries
server._ref_audio_resolve_cache_max_bytes = budget
server._put_resolved_ref_audio("a", np.zeros(2, dtype=np.float32), 24000, "artifact-a")
server._put_resolved_ref_audio("b", np.ones(2, dtype=np.float32), 24000, "artifact-b")
assert list(server._ref_audio_resolve_cache) == ["b"]
assert server._ref_audio_resolve_cache_bytes == 8
@@ -1592,7 +1592,8 @@ class TestTTSMethods:
assert first[1] == 24000
assert second[1] == 24000
assert first[0] is second[0]
assert first[0] == second[0]
assert first[0] is not second[0]
assert first[0][0] == pytest.approx(float(wav[0]), abs=1e-4)
cache_key = first[2]
assert speech_server._get_resolved_ref_audio_artifact_key(
@@ -5430,7 +5431,7 @@ class TestTTSAsyncOffloading:
ref_audio = "data:audio/wav;base64,same"
qwen3_tts_server._put_resolved_ref_audio(
hashlib.sha1(ref_audio.encode("utf-8")).hexdigest(),
wav_list,
np.asarray(wav_list, dtype=np.float32),
24000,
artifact_key,
)
@@ -5466,9 +5467,9 @@ class TestTTSAsyncOffloading:
qwen3_tts_server._ref_audio_resolve_cache_max_entries = 1
qwen3_tts_server._ref_audio_resolve_cache_max_bytes = 1_000_000
qwen3_tts_server._put_resolved_ref_audio("ref-a", [0.0] * 8, 24000, "artifact-a")
qwen3_tts_server._put_resolved_ref_audio("ref-a", np.zeros(8, dtype=np.float32), 24000, "artifact-a")
qwen3_tts_server._ref_audio_model_artifact_ready.add(("artifact-a", False))
qwen3_tts_server._put_resolved_ref_audio("ref-b", [0.0] * 8, 24000, "artifact-b")
qwen3_tts_server._put_resolved_ref_audio("ref-b", np.zeros(8, dtype=np.float32), 24000, "artifact-b")
assert ("artifact-a", False) not in qwen3_tts_server._ref_audio_model_artifact_ready
assert "artifact-b" in {entry[3] for entry in qwen3_tts_server._ref_audio_resolve_cache.values()}
@@ -5715,7 +5716,7 @@ class TestTTSAsyncOffloading:
def test_qwen3_xvector_ready_artifact_does_not_enable_icl_artifact_only(self, qwen3_tts_server):
# An x-vector-only artifact (speaker embedding, no ref_code) must not enable
# the artifact-only path for a later ICL request with the same ref_audio (#5049).
qwen3_tts_server._put_resolved_ref_audio("ref-a", [0.0] * 8, 24000, "artifact-a")
qwen3_tts_server._put_resolved_ref_audio("ref-a", np.zeros(8, dtype=np.float32), 24000, "artifact-a")
qwen3_tts_server._track_ref_audio_artifact_warmup("req-xvec", "artifact-a", x_vector_only=True)
qwen3_tts_server._mark_ref_audio_artifact_ready_for_request("req-xvec")
@@ -5723,7 +5724,7 @@ class TestTTSAsyncOffloading:
assert qwen3_tts_server._adapter._qwen3_tts_can_use_ref_audio_artifact_only(icl_params, "artifact-a") is False
def test_qwen3_xvector_ready_artifact_still_reusable_by_xvector_request(self, qwen3_tts_server):
qwen3_tts_server._put_resolved_ref_audio("ref-a", [0.0] * 8, 24000, "artifact-a")
qwen3_tts_server._put_resolved_ref_audio("ref-a", np.zeros(8, dtype=np.float32), 24000, "artifact-a")
qwen3_tts_server._track_ref_audio_artifact_warmup("req-xvec", "artifact-a", x_vector_only=True)
qwen3_tts_server._mark_ref_audio_artifact_ready_for_request("req-xvec")
@@ -5731,7 +5732,7 @@ class TestTTSAsyncOffloading:
assert qwen3_tts_server._adapter._qwen3_tts_can_use_ref_audio_artifact_only(xvec_params, "artifact-a") is True
def test_qwen3_icl_ready_artifact_enables_icl_artifact_only(self, qwen3_tts_server):
qwen3_tts_server._put_resolved_ref_audio("ref-a", [0.0] * 8, 24000, "artifact-a")
qwen3_tts_server._put_resolved_ref_audio("ref-a", np.zeros(8, dtype=np.float32), 24000, "artifact-a")
qwen3_tts_server._track_ref_audio_artifact_warmup("req-icl", "artifact-a", x_vector_only=False)
qwen3_tts_server._mark_ref_audio_artifact_ready_for_request("req-icl")