Signed-off-by: Sy03 <1370724210@qq.com> Signed-off-by: lishunyang12 <lishunyang03@gmail.com> Signed-off-by: Yueqian Lin <linyueqian@outlook.com> Co-authored-by: lishunyang12 <lishunyang03@gmail.com> Co-authored-by: Yueqian Lin <linyueqian@outlook.com> Co-authored-by: StormMelody <22371184@zju.edu.cn> Co-authored-by: Gao Han <hgaoaf@connect.ust.hk> Co-authored-by: Hongsheng Liu <liuhongsheng4@huawei.com>
277 lines
8.3 KiB
Python
277 lines
8.3 KiB
Python
# SPDX-License-Identifier: Apache-2.0
|
|
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
|
|
|
"""Model-neutral contracts for the experimental duplex engine plugin.
|
|
|
|
This module contains only immutable data transfer objects and narrow protocols.
|
|
Duplex control algorithms, session implementations, model policy, and Realtime
|
|
serving remain in sibling experimental modules.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import base64
|
|
from collections.abc import Iterable, Mapping
|
|
from dataclasses import dataclass, field
|
|
from enum import Enum
|
|
from types import MappingProxyType
|
|
from typing import Any, Protocol
|
|
|
|
from vllm_omni.engine.messages import EngineQueueMessage
|
|
from vllm_omni.experimental.fullduplex.engine.messages import DuplexFence
|
|
|
|
|
|
class SessionMode(str, Enum):
|
|
TURN = "turn"
|
|
DUPLEX = "duplex"
|
|
|
|
|
|
class DuplexInputMode(str, Enum):
|
|
APPEND_TOKENS = "append_tokens"
|
|
APPEND_AUDIO_CHUNK = "append_audio_chunk"
|
|
REPLACE_LATEST_CHUNK = "replace_latest_chunk"
|
|
REENCODE_CONTEXT = "reencode_context"
|
|
ROLLBACK_TO_CHECKPOINT = "rollback_to_checkpoint"
|
|
TURN_COMMIT_ONLY = "turn_commit_only"
|
|
|
|
|
|
class DuplexOutputAction(str, Enum):
|
|
DIRECT_RESPONSE = "direct_response"
|
|
|
|
|
|
@dataclass
|
|
class DuplexRuntimeCapabilities:
|
|
input_modes: set[DuplexInputMode] = field(default_factory=lambda: {DuplexInputMode.TURN_COMMIT_ONLY})
|
|
implementation_level: str = "serving_session_adapter"
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class DuplexAppendPlan:
|
|
prompt: dict[str, Any]
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class DuplexOutputDecision:
|
|
action: DuplexOutputAction
|
|
metadata: Mapping[str, Any] = field(default_factory=dict)
|
|
final_output_type: str = "text"
|
|
|
|
def __post_init__(self) -> None:
|
|
object.__setattr__(self, "metadata", MappingProxyType(dict(self.metadata)))
|
|
|
|
|
|
class DuplexRuntimeExtension(Protocol):
|
|
"""Pure model policy invoked by the experimental duplex control plane."""
|
|
|
|
def configure_sampling_params(
|
|
self,
|
|
*,
|
|
runtime_config: dict[str, Any],
|
|
defaults: tuple[object, ...],
|
|
) -> tuple[object, ...]: ...
|
|
|
|
def plan_append(
|
|
self,
|
|
*,
|
|
request_id: str,
|
|
fence: DuplexFence,
|
|
session_config: dict[str, Any],
|
|
runtime_config: dict[str, Any],
|
|
seq: int,
|
|
turn_seq: int,
|
|
mode: DuplexInputMode,
|
|
payload: object,
|
|
final: bool,
|
|
sampling_params: object,
|
|
) -> DuplexAppendPlan: ...
|
|
|
|
def decide_output(
|
|
self,
|
|
*,
|
|
stage_id: int,
|
|
final_stage_id: int,
|
|
segment_finished: bool,
|
|
segment_token_ids: tuple[int, ...],
|
|
segment_output_metadata: dict[str, Any],
|
|
output: object,
|
|
) -> DuplexOutputDecision | None: ...
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class DuplexRequestIdentity:
|
|
session_id: str
|
|
fence: DuplexFence
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class DuplexStageRequestContext:
|
|
request_id: str
|
|
session_id: str
|
|
fence: DuplexFence
|
|
stage_id: int
|
|
final_stage_id: int
|
|
config_generation: int
|
|
sampling_params: tuple[object, ...]
|
|
session_config: Mapping[str, Any] = field(default_factory=dict)
|
|
runtime_config: Mapping[str, Any] = field(default_factory=dict)
|
|
|
|
def __post_init__(self) -> None:
|
|
object.__setattr__(self, "sampling_params", tuple(self.sampling_params))
|
|
object.__setattr__(self, "session_config", MappingProxyType(dict(self.session_config)))
|
|
object.__setattr__(self, "runtime_config", MappingProxyType(dict(self.runtime_config)))
|
|
|
|
@property
|
|
def stage_sampling_params(self) -> object:
|
|
return self.sampling_params[self.stage_id]
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class DuplexStageSubmission:
|
|
context: DuplexStageRequestContext
|
|
prompt: Mapping[str, Any]
|
|
already_submitted: bool
|
|
|
|
def __post_init__(self) -> None:
|
|
object.__setattr__(self, "prompt", MappingProxyType(dict(self.prompt)))
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class DuplexStageSubmissionResult:
|
|
request_id: str
|
|
stage_id: int
|
|
replica_id: int
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class DuplexOutputContext:
|
|
identity: DuplexRequestIdentity
|
|
final_stage_id: int
|
|
segment_finished: bool
|
|
segment_token_ids: tuple[int, ...] = ()
|
|
segment_output_metadata: Mapping[str, Any] = field(default_factory=dict)
|
|
|
|
def __post_init__(self) -> None:
|
|
object.__setattr__(self, "segment_token_ids", tuple(self.segment_token_ids))
|
|
object.__setattr__(
|
|
self,
|
|
"segment_output_metadata",
|
|
MappingProxyType(dict(self.segment_output_metadata)),
|
|
)
|
|
|
|
|
|
class DuplexStagePort(Protocol):
|
|
@property
|
|
def stage_count(self) -> int: ...
|
|
|
|
def sampling_defaults(self) -> tuple[object, ...]: ...
|
|
|
|
def ensure_request(self, context: DuplexStageRequestContext) -> None: ...
|
|
|
|
async def submit(self, submission: DuplexStageSubmission) -> DuplexStageSubmissionResult: ...
|
|
|
|
async def cleanup(self, request_ids: list[str], *, abort: bool = False) -> None: ...
|
|
|
|
|
|
class DuplexControlPlanePort(Protocol):
|
|
@property
|
|
def sessions(self) -> object: ...
|
|
|
|
def accepts(self, message: object) -> bool: ...
|
|
|
|
def dispatch(self, message: object) -> None: ...
|
|
|
|
async def shutdown(self) -> None: ...
|
|
|
|
def close_sessions_for_request_ids(self, request_ids: list[str]) -> dict[str, list[str]]: ...
|
|
|
|
def finalize_closed_sessions(self, session_ids: Iterable[str]) -> None: ...
|
|
|
|
def session_for_identity(self, identity: DuplexRequestIdentity | None) -> object | None: ...
|
|
|
|
def decide_output(
|
|
self,
|
|
stage_id: int,
|
|
output: object,
|
|
context: DuplexOutputContext | None,
|
|
) -> DuplexOutputDecision | None: ...
|
|
|
|
|
|
class CorrelatedRpcTransport(Protocol):
|
|
def execute(
|
|
self,
|
|
key: tuple[str, str],
|
|
message: EngineQueueMessage,
|
|
*,
|
|
timeout: float | None,
|
|
timeout_message: str,
|
|
block_on_submit: bool = False,
|
|
) -> EngineQueueMessage: ...
|
|
|
|
|
|
def duplex_data_plane_request_info(result: dict[str, object]) -> tuple[str | None, int | None]:
|
|
stage_results = result.get("stage_results")
|
|
if not isinstance(stage_results, list):
|
|
return None, None
|
|
for item in stage_results:
|
|
if not isinstance(item, dict):
|
|
continue
|
|
inner = item.get("result")
|
|
if not isinstance(inner, dict) or inner.get("data_plane_append") is not True:
|
|
continue
|
|
request_id = inner.get("request_id")
|
|
if not isinstance(request_id, str) or not request_id:
|
|
continue
|
|
response_stage_id = inner.get("response_stage_id")
|
|
return request_id, response_stage_id if isinstance(response_stage_id, int) else None
|
|
return None, None
|
|
|
|
|
|
def duplex_resource_request_id(fence: DuplexFence, role: str) -> str:
|
|
if not role or any(
|
|
character not in "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-" for character in role
|
|
):
|
|
raise ValueError(f"invalid duplex resource role: {role!r}")
|
|
encoded_session_id = base64.urlsafe_b64encode(fence.session_id.encode("utf-8")).decode("ascii").rstrip("=")
|
|
return f"duplex-s.{encoded_session_id}.i.{fence.incarnation}.e.{fence.epoch}.r.{role}"
|
|
|
|
|
|
def duplex_resource_request_belongs_to_session(request_id: str, session_id: str) -> bool:
|
|
"""Return whether a current-format resource request belongs to a session."""
|
|
parts = request_id.split(".")
|
|
if len(parts) != 8 or parts[0] != "duplex-s" or parts[2] != "i" or parts[4] != "e" or parts[6] != "r":
|
|
return False
|
|
try:
|
|
int(parts[3])
|
|
int(parts[5])
|
|
except ValueError:
|
|
return False
|
|
role = parts[7]
|
|
if not role or any(
|
|
character not in "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-" for character in role
|
|
):
|
|
return False
|
|
encoded_session_id = base64.urlsafe_b64encode(session_id.encode("utf-8")).decode("ascii").rstrip("=")
|
|
return parts[1] == encoded_session_id
|
|
|
|
|
|
__all__ = [
|
|
"CorrelatedRpcTransport",
|
|
"DuplexAppendPlan",
|
|
"DuplexControlPlanePort",
|
|
"DuplexInputMode",
|
|
"DuplexOutputAction",
|
|
"DuplexOutputContext",
|
|
"DuplexOutputDecision",
|
|
"DuplexRequestIdentity",
|
|
"DuplexRuntimeCapabilities",
|
|
"DuplexRuntimeExtension",
|
|
"DuplexStagePort",
|
|
"DuplexStageRequestContext",
|
|
"DuplexStageSubmission",
|
|
"DuplexStageSubmissionResult",
|
|
"SessionMode",
|
|
"duplex_data_plane_request_info",
|
|
"duplex_resource_request_belongs_to_session",
|
|
"duplex_resource_request_id",
|
|
]
|