168 lines
6.7 KiB
Python
168 lines
6.7 KiB
Python
# SPDX-License-Identifier: Apache-2.0
|
|
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
|
|
|
"""Deliver in-stage first audio as soon as its device copy completes.
|
|
|
|
The regular output of a step is published only after the whole step's
|
|
outputs are copied, materialized for every request, popped by the engine loop
|
|
and serialized together. A stream's first audio chunk does not need any of
|
|
that, so a dedicated thread waits for the chunk's own copy event and hands a
|
|
one-request output straight to the engine core's output queue.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import queue
|
|
import threading
|
|
from dataclasses import dataclass
|
|
from typing import Any, Protocol
|
|
|
|
import torch
|
|
from vllm.logger import init_logger
|
|
|
|
from vllm_omni.data_entry_keys import FIRST_AUDIO_KEY
|
|
|
|
logger = init_logger(__name__)
|
|
|
|
|
|
class FirstAudioSink(Protocol):
|
|
def prepare(self, request_ids: list[str]) -> _PreparedDelivery:
|
|
"""Freeze request routes on the submitting thread before accepting delivery."""
|
|
...
|
|
|
|
|
|
class FirstAudioSender:
|
|
def __init__(self, sink: FirstAudioSink) -> None:
|
|
if not callable(getattr(sink, "prepare", None)):
|
|
raise TypeError("FirstAudioSender requires a sink with prepare(request_ids)")
|
|
self._sink = sink
|
|
self._queue: queue.SimpleQueue[
|
|
tuple[torch.cuda.Event, torch.Tensor, list[str], torch.Tensor, torch.Tensor | None, _PreparedDelivery]
|
|
| None
|
|
]
|
|
self._queue = queue.SimpleQueue()
|
|
self._thread = threading.Thread(target=self._run, daemon=True, name="omni-first-audio-sender")
|
|
self._thread.start()
|
|
|
|
def submit(
|
|
self,
|
|
request_ids: list[str],
|
|
pcm: torch.Tensor,
|
|
sample_rate: torch.Tensor,
|
|
valid: torch.Tensor | None = None,
|
|
) -> list[str]:
|
|
"""Queue a D2H copy of ``pcm`` [n, samples] on the current stream and deliver it once done.
|
|
|
|
``valid`` [n] (on device) drops rows whose frame turned out not to be
|
|
audio (e.g. a codec EOS sample); it is read only after the copy.
|
|
|
|
Return the request IDs whose delivery was accepted. Rows without an
|
|
engine output route must retain regular codec delivery instead of
|
|
promising the orchestrator a first frame that can never arrive.
|
|
"""
|
|
delivery = self._sink.prepare(request_ids)
|
|
accepted = list(delivery.routes)
|
|
if not accepted:
|
|
return []
|
|
host = torch.empty(pcm.shape, dtype=pcm.dtype, pin_memory=True)
|
|
host.copy_(pcm, non_blocking=True)
|
|
host_valid = None
|
|
if valid is not None:
|
|
host_valid = torch.empty(valid.shape, dtype=torch.bool, pin_memory=True)
|
|
host_valid.copy_(valid, non_blocking=True)
|
|
copied = torch.cuda.Event()
|
|
copied.record()
|
|
self._queue.put((copied, host, list(request_ids), sample_rate, host_valid, delivery))
|
|
return accepted
|
|
|
|
def close(self) -> None:
|
|
self._queue.put(None)
|
|
self._thread.join(timeout=5)
|
|
|
|
def _run(self) -> None:
|
|
while (item := self._queue.get()) is not None:
|
|
copied, host, request_ids, sample_rate, host_valid, delivery = item
|
|
pending = request_ids
|
|
try:
|
|
copied.synchronize()
|
|
rows = [row for row in range(len(request_ids)) if host_valid is None or bool(host_valid[row])]
|
|
pending = [request_ids[row] for row in rows]
|
|
if rows:
|
|
delivery(pending, [host[row].clone() for row in rows], sample_rate)
|
|
except Exception:
|
|
# If synchronization failed, validity is not readable yet.
|
|
# Otherwise only valid, not-yet-delivered rows owe first audio.
|
|
logger.exception("First-audio delivery failed for %s", pending)
|
|
delivery.fail(pending)
|
|
|
|
|
|
@dataclass
|
|
class _PreparedDelivery:
|
|
sink: _EngineOutputSink
|
|
# Frozen client indices for outstanding deliveries. Successful enqueues
|
|
# retire their routes so a later failure cannot abort completed deliveries.
|
|
routes: dict[str, int]
|
|
|
|
def __call__(self, request_ids: list[str], pcm_rows: list[torch.Tensor], sample_rate: torch.Tensor) -> None:
|
|
self.sink._send(self.routes, request_ids, pcm_rows, sample_rate)
|
|
|
|
def fail(self, request_ids: list[str]) -> None:
|
|
self.sink._fail(self.routes, request_ids)
|
|
|
|
|
|
class _EngineOutputSink:
|
|
def __init__(self, output_queue: Any, scheduler: Any) -> None:
|
|
self.output_queue = output_queue
|
|
self.scheduler = scheduler
|
|
|
|
def prepare(self, request_ids: list[str]) -> _PreparedDelivery:
|
|
routes = {}
|
|
for request_id in request_ids:
|
|
request = self.scheduler.requests.get(request_id)
|
|
if request is not None:
|
|
routes[request_id] = int(getattr(request, "client_index", 0) or 0)
|
|
return _PreparedDelivery(self, routes)
|
|
|
|
def _send(self, routes: dict[str, int], request_ids, pcm_rows, sample_rate) -> None:
|
|
from vllm_omni.engine import OmniEngineCoreOutput, OmniEngineCoreOutputs
|
|
|
|
by_client: dict[int, list[OmniEngineCoreOutput]] = {}
|
|
for request_id, pcm in zip(request_ids, pcm_rows, strict=True):
|
|
if request_id not in routes:
|
|
continue
|
|
by_client.setdefault(routes[request_id], []).append(
|
|
OmniEngineCoreOutput(
|
|
request_id=request_id,
|
|
new_token_ids=[],
|
|
multimodal_output={
|
|
"model_outputs": pcm,
|
|
"sr": sample_rate,
|
|
FIRST_AUDIO_KEY: torch.tensor(True),
|
|
},
|
|
)
|
|
)
|
|
for client_index, outputs in by_client.items():
|
|
self.output_queue.put_nowait((client_index, OmniEngineCoreOutputs(outputs=outputs)))
|
|
for output in outputs:
|
|
routes.pop(output.request_id)
|
|
|
|
def _fail(self, routes: dict[str, int], request_ids: list[str]) -> None:
|
|
from vllm.v1.engine import FinishReason
|
|
|
|
from vllm_omni.engine import OmniEngineCoreOutput, OmniEngineCoreOutputs
|
|
|
|
for request_id in request_ids:
|
|
if request_id in routes:
|
|
output = OmniEngineCoreOutput(
|
|
request_id=request_id,
|
|
new_token_ids=[],
|
|
finish_reason=FinishReason.ERROR,
|
|
stop_reason="First-audio delivery failed",
|
|
)
|
|
self.output_queue.put_nowait((routes[request_id], OmniEngineCoreOutputs(outputs=[output])))
|
|
routes.pop(request_id)
|
|
|
|
|
|
def engine_output_queue_sink(output_queue: Any, scheduler: Any) -> _EngineOutputSink:
|
|
return _EngineOutputSink(output_queue, scheduler)
|