[Core][Frontend] Unified Full-duplex Framework (#7413)
Signed-off-by: chickeyton <ngton2014@gmail.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
9005d78903
commit
99ff4f3070
182 files changed
+26951
-34743
No files matched your search
@@ -5,7 +5,7 @@ from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from vllm_omni.entrypoints.duplex.commit_policy import (
|
||||
from vllm_omni.engine.duplex.session.commit_policy import (
|
||||
CommitAction,
|
||||
CommitSnapshot,
|
||||
decide_commit_action,
|
||||
@@ -24,7 +24,7 @@ pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
|
||||
speech_since_commit=True,
|
||||
active_response_id=None,
|
||||
overlap_speech_ms=0,
|
||||
native_response_in_progress=False,
|
||||
response_in_progress=False,
|
||||
playback_active=False,
|
||||
),
|
||||
CommitAction.START_AUTO_RESPONSE,
|
||||
@@ -36,7 +36,7 @@ pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
|
||||
speech_since_commit=True,
|
||||
active_response_id=None,
|
||||
overlap_speech_ms=800,
|
||||
native_response_in_progress=True,
|
||||
response_in_progress=True,
|
||||
playback_active=True,
|
||||
),
|
||||
CommitAction.START_AUTO_RESPONSE,
|
||||
@@ -48,7 +48,7 @@ pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
|
||||
speech_since_commit=False,
|
||||
active_response_id=None,
|
||||
overlap_speech_ms=0,
|
||||
native_response_in_progress=True,
|
||||
response_in_progress=True,
|
||||
playback_active=False,
|
||||
),
|
||||
CommitAction.COMMIT_ONLY,
|
||||
@@ -60,7 +60,7 @@ pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
|
||||
speech_since_commit=True,
|
||||
active_response_id=None,
|
||||
overlap_speech_ms=800,
|
||||
native_response_in_progress=True,
|
||||
response_in_progress=True,
|
||||
playback_active=False,
|
||||
),
|
||||
CommitAction.DEFER_ACTIVE_RESPONSE,
|
||||
@@ -72,7 +72,7 @@ pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
|
||||
speech_since_commit=True,
|
||||
active_response_id=None,
|
||||
overlap_speech_ms=800,
|
||||
native_response_in_progress=False,
|
||||
response_in_progress=False,
|
||||
playback_active=True,
|
||||
),
|
||||
CommitAction.DEFER_ACTIVE_RESPONSE,
|
||||
@@ -84,7 +84,7 @@ pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
|
||||
speech_since_commit=True,
|
||||
active_response_id=None,
|
||||
overlap_speech_ms=0,
|
||||
native_response_in_progress=False,
|
||||
response_in_progress=False,
|
||||
playback_active=False,
|
||||
),
|
||||
CommitAction.COMMIT_ONLY,
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
|
||||
"""The cumulative Stage1 audio cursor.
|
||||
|
||||
Stage1 hands the data plane a snapshot of every sample it has generated for the
|
||||
request so far, and only the new tail may reach the client. The cursor that
|
||||
makes that possible is the one piece of per-request state whose loss is silent
|
||||
and expensive: rewinding it re-sends the whole session as a single delta.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from vllm_omni.model_executor.models.minicpmo_4_5.duplex.data_plane import (
|
||||
MiniCPMO45DataPlaneSession,
|
||||
)
|
||||
|
||||
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
|
||||
|
||||
REQUEST_ID = "duplex-s.c2Vzc2lvbg==.e.0.r.stage0"
|
||||
|
||||
|
||||
def _session() -> MiniCPMO45DataPlaneSession:
|
||||
return MiniCPMO45DataPlaneSession(lambda *args, **kwargs: "")
|
||||
|
||||
|
||||
def _snapshot(num_samples: int) -> np.ndarray:
|
||||
return np.arange(num_samples, dtype=np.float32)
|
||||
|
||||
|
||||
def _sliced_len(session: MiniCPMO45DataPlaneSession, num_samples: int) -> int:
|
||||
sliced = session.slice_cumulative_audio(REQUEST_ID, _snapshot(num_samples))
|
||||
if sliced is None:
|
||||
return 0
|
||||
return int(np.asarray(sliced).reshape(-1).size)
|
||||
|
||||
|
||||
def test_a_growing_snapshot_yields_only_the_new_tail() -> None:
|
||||
session = _session()
|
||||
assert _sliced_len(session, 24000) == 24000
|
||||
assert _sliced_len(session, 48000) == 24000
|
||||
assert _sliced_len(session, 48000) == 0 # unchanged snapshot emits nothing
|
||||
|
||||
|
||||
def test_a_placeholder_unit_does_not_rewind_the_cursor() -> None:
|
||||
"""The failure behind a 300s benchmark case producing 1373s of audio.
|
||||
|
||||
A continuation unit arrives as a one-sample snapshot. Taking that as "the
|
||||
producer restarted" drops the cursor to 1, so the next real snapshot is
|
||||
sliced from the start and the client is handed the entire session again.
|
||||
"""
|
||||
session = _session()
|
||||
assert _sliced_len(session, 637440) == 637440
|
||||
assert _sliced_len(session, 1) == 1
|
||||
assert _sliced_len(session, 1) == 1
|
||||
assert _sliced_len(session, 1) == 1
|
||||
|
||||
# 649920 - 637440, not 649920 - 1.
|
||||
assert _sliced_len(session, 649920) == 12480
|
||||
|
||||
|
||||
def test_a_genuinely_restarted_producer_is_still_picked_up() -> None:
|
||||
"""Two consecutive below-cursor snapshots that grow are a new buffer."""
|
||||
session = _session()
|
||||
assert _sliced_len(session, 48000) == 48000
|
||||
assert _sliced_len(session, 1000) == 1000 # head of the new buffer
|
||||
assert _sliced_len(session, 3000) == 2000 # its tail, not 3000 - 48000
|
||||
assert _sliced_len(session, 5000) == 2000
|
||||
|
||||
|
||||
def test_close_stream_releases_the_cursor_for_the_next_stream() -> None:
|
||||
session = _session()
|
||||
assert _sliced_len(session, 48000) == 48000
|
||||
session.close_stream(REQUEST_ID)
|
||||
assert _sliced_len(session, 24000) == 24000
|
||||
@@ -55,20 +55,18 @@ class TestRegistryDeclaration:
|
||||
assert pipeline.model_type == _PIPELINE_KEY
|
||||
assert pipeline.model_arch == "MiniCPMO45OmniForConditionalGeneration"
|
||||
|
||||
def test_native_duplex_control_is_explicit_without_a_fixed_session_cap(self) -> None:
|
||||
def test_duplex_plugin_is_declared_without_a_fixed_session_cap(self) -> None:
|
||||
pipeline = OMNI_PIPELINES[_PIPELINE_KEY]
|
||||
assert pipeline.duplex_control_enabled is True
|
||||
assert pipeline.duplex_serving_adapter == (
|
||||
"vllm_omni.model_executor.models.minicpmo_4_5.duplex.serving_adapter.MiniCPMO45ServingRuntimeAdapter"
|
||||
assert pipeline.duplex_plugin == (
|
||||
"vllm_omni.model_executor.models.minicpmo_4_5.duplex.plugin.MiniCPMO45DuplexPlugin"
|
||||
)
|
||||
assert not hasattr(pipeline, "max_native_duplex_sessions")
|
||||
|
||||
def test_ordinary_pipeline_defaults_to_no_duplex_control(self) -> None:
|
||||
def test_ordinary_pipeline_declares_no_duplex_plugin(self) -> None:
|
||||
pipeline = PipelineConfig(
|
||||
model_type="ordinary", stages=(StagePipelineConfig(stage_id=0, model_stage="a", final_output=True),)
|
||||
)
|
||||
assert pipeline.duplex_control_enabled is False
|
||||
assert pipeline.duplex_serving_adapter is None
|
||||
assert pipeline.duplex_plugin is None
|
||||
assert not hasattr(pipeline, "max_native_duplex_sessions")
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
|
||||
"""Nemotron VoiceChat duplex is not on the duplex plugin framework yet.
|
||||
|
||||
RFC vllm-omni#7181 splits the port: this PR lands the unified framework with
|
||||
MiniCPM-o 4.5, and the follow-up PR moves Nemotron VoiceChat onto
|
||||
``DuplexModelPlugin``. Until then the modules under test still import the
|
||||
pre-framework duplex runtime / serving adapter that this PR removed, so
|
||||
collecting them would fail at import time and abort the whole pytest session.
|
||||
The tests themselves are unchanged and come back with the port.
|
||||
|
||||
Listed per file rather than globbed, so the coverage loss is visible and shrinks
|
||||
file by file as the port lands. ``test_input.py`` imports nothing that was
|
||||
removed and keeps running.
|
||||
"""
|
||||
|
||||
#: Both import ``DuplexFence`` from ``engine.duplex.messages``, which no longer
|
||||
#: exports it; ``test_data_plane`` also imports ``engine.duplex.runtime``.
|
||||
collect_ignore = ["test_data_plane.py", "test_runtime_contract.py"]
|
||||
@@ -7,9 +7,9 @@ from types import SimpleNamespace
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
from vllm_omni.engine.duplex.runtime import duplex_resource_request_id
|
||||
|
||||
from vllm_omni.engine.duplex.messages import DuplexFence
|
||||
from vllm_omni.engine.duplex.runtime import duplex_resource_request_id
|
||||
from vllm_omni.model_executor.models.nemotron_voicechat.duplex.data_plane import (
|
||||
NemotronVoiceChatDataPlaneContext,
|
||||
NemotronVoiceChatDataPlaneSession,
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
|
||||
"""PersonaPlex duplex is not on the duplex plugin framework yet.
|
||||
|
||||
RFC vllm-omni#7181 splits the port: this PR lands the unified framework with
|
||||
MiniCPM-o 4.5, and the follow-up PR moves PersonaPlex onto ``DuplexModelPlugin``.
|
||||
Until then the modules under test still import the pre-framework duplex
|
||||
runtime / serving adapter that this PR removed, so collecting them would fail
|
||||
at import time and abort the whole pytest session. The tests themselves are
|
||||
unchanged and come back with the port.
|
||||
|
||||
Listed per file rather than globbed, so the coverage loss is visible and shrinks
|
||||
file by file as the port lands.
|
||||
"""
|
||||
|
||||
#: ``test_unified_runtime`` imports ``engine.duplex.runtime`` and
|
||||
#: ``entrypoints.duplex.runtime_adapter`` directly. ``test_stage0_runtime`` fails
|
||||
#: transitively: ``personaplex.duplex.runtime_extension`` imports ``DuplexFence``
|
||||
#: from ``engine.duplex.messages``, which no longer exports it.
|
||||
collect_ignore = ["test_stage0_runtime.py", "test_unified_runtime.py"]
|
||||
@@ -8,6 +8,12 @@ from types import SimpleNamespace
|
||||
import numpy as np
|
||||
import pytest
|
||||
from vllm.sampling_params import SamplingParams
|
||||
from vllm_omni.engine.duplex.runtime import (
|
||||
DuplexInputMode,
|
||||
)
|
||||
from vllm_omni.entrypoints.duplex.runtime_adapter import (
|
||||
ServingRuntimeConfigError,
|
||||
)
|
||||
|
||||
from tests.e2e.online_serving import personaplex_realtime_duplex as e2e_driver
|
||||
from vllm_omni.config.stage_config import (
|
||||
@@ -15,12 +21,6 @@ from vllm_omni.config.stage_config import (
|
||||
merge_pipeline_deploy,
|
||||
)
|
||||
from vllm_omni.engine.duplex.messages import DuplexFence
|
||||
from vllm_omni.engine.duplex.runtime import (
|
||||
DuplexInputMode,
|
||||
)
|
||||
from vllm_omni.entrypoints.duplex.runtime_adapter import (
|
||||
ServingRuntimeConfigError,
|
||||
)
|
||||
from vllm_omni.model_executor.models.personaplex.duplex.config import DEFAULT_PERSONA
|
||||
from vllm_omni.model_executor.models.personaplex.duplex.data_plane import (
|
||||
PersonaPlexDataPlaneContext,
|
||||
|
||||
Reference in new issue
Block a user