[Core][Frontend] Bound duplex output delivery and reject cancelled audio (#7644)
Signed-off-by: Nolen Liang <nliang@nvidia.com> Signed-off-by: bcsdhjew <nliang@nvidia.com>
This commit is contained in:
1 parent
3f20252151
commit
f781f7bb08
26 files changed
+1170
-147
No files matched your search
@@ -220,6 +220,7 @@ vllm_omni/
|
||||
│ │ DUPLEX_REALTIME_CAPABILITIES, duplex_response_format
|
||||
│ ├── events.py re-export shim (DuplexEvent = RealtimeEvent) + the runner's
|
||||
│ │ epoch-filter sets DOMAIN_TERMINAL_EVENTS / MODEL_OUTPUT_EVENTS
|
||||
│ ├── delivery.py DuplexOutputBuffer (bounded output shared with the session handle)
|
||||
│ ├── realtime_events.py RealtimeProjectionState: internal event -> typed events
|
||||
│ ├── messages.py queue envelopes (Open/Close/Resume/Touch/Command/Result/Event), DuplexSessionError
|
||||
│ ├── config.py DuplexSessionConfig, DuplexCapabilities, ResponseCreateOptions
|
||||
@@ -284,7 +285,8 @@ command handle.submit(DuplexCommand)
|
||||
-> engine.submit_command_async -> DuplexSessionCommandMessage on the request queue [one-way]
|
||||
-> DuplexSessionManager.dispatch: unknown_session / input_backpressure checks, then runner mailbox
|
||||
output DuplexOrchestrator._intercept_stage_output -> runner.on_stage_output -> mailbox -> typed events
|
||||
-> output_sink (DuplexSessionEventMessage) -> DuplexOmni._route_engine_message -> handle.events()
|
||||
-> session's bounded output buffer -> handle.events(); closure and errors without a live session
|
||||
use output_sink (DuplexSessionEventMessage) -> DuplexOmni._route_engine_message
|
||||
detach DuplexOmni.detach_session(expected_lease_generation) -> touch(DETACH): engine-owned disconnect
|
||||
grace, refused for a lease newer than the one the caller held; expiry -> SessionExpired
|
||||
resume DuplexOmni.resume_session(expected_lease_generation) -> lease CAS keyed by the control id (a replay
|
||||
@@ -303,7 +305,9 @@ reap DuplexSessionManager.reaper_loop: idle TTL / disconnect grace expiry, c
|
||||
|
||||
### Concurrency and ordering model of the runner
|
||||
|
||||
Everything below runs on the orchestrator asyncio loop; there is no lock.
|
||||
The runner's session state is owned by the orchestrator asyncio loop and
|
||||
needs no lock. The output buffer shared with the caller thread uses a lock
|
||||
for queue updates and the final validity check before delivery.
|
||||
|
||||
- **Single writer per session.** `DuplexEngineSession` is mutated only by its
|
||||
runner. Inputs reach the runner through one mailbox in this order: client
|
||||
@@ -332,13 +336,15 @@ Everything below runs on the orchestrator asyncio loop; there is no lock.
|
||||
on one loop in program order, nothing can be emitted for an old epoch after
|
||||
its terminal event; `runner.emit()` still drops a terminal carrying a stale
|
||||
epoch and stamps `epoch` on every event for clients that filter
|
||||
defensively. This is a statement about order, not about latency: the engine
|
||||
output queue and each `DuplexSessionHandle` outbox are unbounded and FIFO,
|
||||
so a cancellation is delivered behind whatever audio was already emitted for
|
||||
the response it cancels. A client that must stop quickly cancels its own
|
||||
playback on `response.done` / `audio.cancelled` rather than waiting for the
|
||||
stream to drain. Bounding those buffers and letting an accepted invalidation
|
||||
skip undelivered media is left to the follow-up RFC.
|
||||
defensively. In this experimental follow-up, public output goes directly to
|
||||
one bounded buffer shared by the engine and `DuplexSessionHandle`. Accepted
|
||||
cancellation removes queued `AudioDelta` events for that response through
|
||||
the cancelled epoch. Text increments, audio/transcript completion markers,
|
||||
other responses and normal completion retain FIFO order. The consumer also
|
||||
checks an event already taken from the buffer: WebSocket delivery repeats
|
||||
that check under the connection lock, before sequencing and journaling.
|
||||
Already sequenced replay entries are unchanged; already delivered audio
|
||||
still requires the client to stop its own playback.
|
||||
- **Backpressure before the mailbox.** `DuplexSessionManager.dispatch`
|
||||
checks `max_pending_input_bytes_per_session` and reserves a pending turn for
|
||||
`Commit` before the put; a rejected command is answered with
|
||||
@@ -349,6 +355,46 @@ Everything below runs on the orchestrator asyncio loop; there is no lock.
|
||||
`ResponseDone.stage_metrics` and `metadata.vllm_omni.stage_metrics` on the
|
||||
wire keep their meaning.
|
||||
|
||||
### Experimental output limits
|
||||
|
||||
`max_pending_output_bytes_per_session` defaults to 2 MiB and
|
||||
`max_pending_output_events_per_session` to 512. The byte limit counts compact
|
||||
Realtime JSON, including base64 audio, not decoded PCM or Python object
|
||||
overhead. Audio is already base64-encoded by the producer; counting its length
|
||||
avoids serializing or decoding that large field again. Neither default has
|
||||
been tuned from a performance study.
|
||||
|
||||
Errors and response endings have an additional 64 KiB / eight-event reserve
|
||||
and retain FIFO order. One final session-closure notification has an independent
|
||||
slot, so an exhausted reserve cannot prevent closure or break delivery to
|
||||
other sessions. If that reserve also fills, an error or response-ending
|
||||
notification may be omitted; the final session-closure notification still
|
||||
has its independent slot. Oversized close details (over 4 KiB with generated identity)
|
||||
are replaced by a compact reason, preserving terminal type and identity.
|
||||
Late writes after closure are ignored; a locally ended iterator is not reopened
|
||||
by a late terminal, and a failed open closes the abandoned handle. Events other
|
||||
than errors are not forwarded once their session's buffer has been removed.
|
||||
One event held by the consumer is outside the queued budget.
|
||||
|
||||
The overflow policy fails the active response without committing its partial
|
||||
text to history and closes only the affected session. If overflow occurs while
|
||||
a completion batch is being delivered, its not-yet-queued `ResponseDone` is
|
||||
converted to failure with its identity and output preserved; any history entry
|
||||
for that response is removed. An ending already accepted by the buffer is not
|
||||
replaced or followed by a second ending. After overflow, newly
|
||||
emitted ordinary events are suppressed, including text/audio completion,
|
||||
content-part and output-item completion markers. Previously queued non-audio
|
||||
events retain FIFO order; error and failed response-ending notifications may
|
||||
use the reserve before the independent closure notification. This differs from
|
||||
accepted cancellation, which removes only matching audio. See the
|
||||
[public error and closure fields](../serving/realtime_duplex_api.md#output-limits-and-slow-consumers).
|
||||
Response-only recovery is not implemented. These limits do not bound the runner's raw stage
|
||||
output mailbox, replay journal, or client playback queues. The shared buffer
|
||||
requires the current same-process, separate-thread engine arrangement; it is
|
||||
not a cross-process delivery protocol. Direct handle iteration and WebSocket
|
||||
delivery share buffer validity, but end-to-end client behavior still needs
|
||||
independent real-model verification.
|
||||
|
||||
## Orchestrator seams
|
||||
|
||||
`OrchestratorBase` keeps the generic machinery (request handler skeleton,
|
||||
|
||||
Reference in new issue
Block a user