Skip to content

[Frontend] Streaming session API (/v1/streaming) - #53879

Draft
nvbfalk wants to merge 4 commits into
vllm-project:mainfrom
nvbfalk:pr4-streaming-session-api
Draft

nvbfalk wants to merge 4 commits into
vllm-project:mainfrom
nvbfalk:pr4-streaming-session-api

Conversation

@nvbfalk

@nvbfalk nvbfalk commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Purpose

Draft session frontend for RFC #51948, on top of the bounded-session core PR
(#53876 — this branch is stacked on it, so its diff shows up here until it merges;
this PR's own commit is +1,326, of which 291 are tests). Gated behind a new
--enable-streaming flag, off by default; with the flag off, no route is
registered and nothing changes.

The API is the smallest lifecycle that makes the core usable:

  • POST /v1/streaming/sessions — open a session (task prompt, retention budgets,
    sampling); returns a session id.
  • POST /v1/streaming/sessions/{id}/frame — push one chunk (a JPEG frame or a text
    turn, raw request body); blocks until that chunk's reply is complete and returns
    it, with per-frame timing. One chunk in flight per session by design: the client
    paces the engine, so replies always stay aligned with the chunks they answer.
  • DELETE /v1/streaming/sessions/{id} — close. Sessions also close on client
    disconnect and on engine error (fail-closed).

Startup guards fail fast instead of dying at first use: the multimodal processor
cache is forced off (a later identical frame must not hit a data-less cache entry
for content whose KV was evicted), async scheduling defaults off, and pipeline
parallelism / multiple API servers are rejected (the session registry is
per-process).

The transport shape is a proposal, not a position.: it could equally be a video
modality on /v1/realtime (#33187) or converge with the RTSP ingest path in #50570.
The session layer here is a thin adapter over AsyncLLM.generate(stream), so whichever shape maintainers prefer,
the others become adapters.

Test Plan

pytest tests/v1/streaming/test_caption_server.py

12 tests: session lifecycle, unknown-session 404s, disconnect cleanup,
concurrent-session rejection, retention-parameter plumbing, oversized-frame
rejection.

E2E: vllm serve Qwen/Qwen3-VL-8B-Instruct --enable-streaming --limit-mm-per-prompt '{"image": 16}' on an H100, driving synthetic frames
through one session.

Test Result

  • 12/12 tests pass (CPU and H100); pre-existing suite failure sets identical to
    main.
  • E2E, 16 frames with a 4-frame retention window: every frame answered correctly
    (the model reads each frame's content, including all frames past the window),
    TTFT 322 ms for the first frame and flat ~40 ms for every frame after.
  • E2E, 300 frames: session stays alive and memory-bounded end to end, TTFT still
    flat at 41 ms at frame 300. The startup guards were observed doing their job in
    the server log.

Portions of this work were developed with AI assistance; all of it has been
reviewed and tested by me.

nvbfalk and others added 4 commits August 25, 2026 09:30
Adds a modality-agnostic primitive to free a block-aligned token range
of a request that stays alive, for long-running streaming-input
sessions (vllm-project#28973) that must bound per-session KV:

- SingleTypeKVCacheManager.evict_blocks_for_request(request_id,
  block_ids): frees the given blocks from a live request's block table,
  removes them from the prefix-cache index so their content can never
  be served to another request, and keeps num_cached_block consistent.
- KVCacheCoordinator.evict_blocks_for_request: fan-out per group.
- KVCacheManager.evict_token_range_for_request(request_id, start, end)
  -> (aligned_start, aligned_end, num_freed): rounds the range INWARD
  to whole blocks and returns the range actually freed, so the caller
  compacts its own per-token arrays by exactly the same span and the
  block table and token bookkeeping cannot go out of sync. Guards the
  hybrid (multi-group) case with an assert rather than fanning one
  token range to groups of different block sizes.

No behavior change for requests that do not call it. Consumer follows
in the bounded-session retention PR (RFC vllm-project#51948); the primitive is
also usable for StreamingLLM-style sink+window text sessions or
pruning stale tool-call context from a live session.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Benedikt Falk <bfalk@nvidia.com>
Four independent fixes for resumable (streaming-input, vllm-project#28973) sessions,
found running multi-hour video-streaming sessions:

1. RequestOutputCollector merges DELTA outputs when the producer gets
   ahead of the consumer, and RequestOutput.add() overwrites
   finish_reason. Per-chunk boundaries are conveyed as transient
   finish_reason markers on non-finished outputs, so a merge across a
   boundary silently erases the marker and the consumer loses a chunk
   boundary. Replace the single-slot collector with a deque that never
   merges into an entry that is finished or carries a finish_reason.

2. process_outputs() treats ANY finish_reason on a streaming-input
   request as a chunk boundary. An engine-initiated ABORT or ERROR is
   terminal for the session; masking it wedges the consumer and desyncs
   the frontend and scheduler chunk queues. Gate boundary handling on
   the finish reason and route an abort back to the engine core.

3. Abort races: cancel AND await the input-stream task before aborting
   (close() only cancels, leaving a window where an in-flight chunk ADD
   is sent after the abort), and mark first chunks on the wire
   (EngineCoreRequest.first_chunk) so the scheduler rejects non-first
   chunks for unknown ids instead of resurrecting the finished session
   as an unabortable request that pins its KV blocks.

4. Zero-chunk streams put STREAM_FINISHED instead of running the
   placeholder final request as a real generation; sticky DP routing
   keeps all chunks of a session on the engine that holds it; sessions
   salt their prefix-cache hash chain (chunk folds mutate block content
   under unchanged hashes).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Benedikt Falk <bfalk@nvidia.com>
…KV eviction, consolidation

Engine core for RFC vllm-project#51948: give a long-running streaming-input session
(vllm-project#28973) a constant memory footprint, for continuous video understanding
and StreamingLLM-style sink+window text sessions.

Mechanism (shared between text and multimodal sessions):
- vllm/v1/streaming/: retention policy (per-session HistorySegment
  bookkeeping + sliding-window budgets), eviction (drop the oldest
  segments of a LIVE session in place via the KV range-free primitive,
  releasing encoder-cache entries and compacting all per-token arrays in
  lockstep by the block-aligned range actually freed), consolidation
  (self-preempting re-prefill of the surviving window before the model's
  trained position range is exceeded), chunk-relative position helpers.
- Survivors keep their original positions; the position space develops
  gaps. Cached K stays rotationally consistent with no kernel changes.
  mRoPE models carry a per-token position tensor, extended per chunk from
  max_cached_position + 1 and sliced on eviction. 1D-RoPE (text) models
  need only a per-request scalar: RoPE position = index + cumulative
  evicted width, reset at re-prefill (the degenerate case of the same
  design, so LLM and VLM sessions share the mechanics end to end).
- Scheduler: segment recording at chunk folds, over-budget eviction,
  re-prefill trigger + self-preemption (phantom-sample discard on idle
  triggers), fold-time cumulative context-length guard, grammar soft-stop
  for sessions, admission checks (single KV-cache group, no async
  scheduling / PP / DP for retention sessions).
- Worker: position slice/extend/reset mirroring the scheduler,
  max_cached_position watermark reporting, persistent encoder cache for
  re-prefill (immediate physical eviction of dead entries), orphan
  mm-placeholder neutralization, streaming-aware encoder cache floor.

Sessions that do not set streaming_retention (in
SamplingParams.extra_args) take none of these paths.

Draft caveats: 1D-RoPE position shift is applied to the shared positions
buffer post-slot-mapping; backends consuming
CommonAttentionMetadata.positions for masking are audited for the
default FA path only. GPU E2E evidence to follow.

86 unit tests (tests/v1/streaming/). Existing suite failure sets are
byte-identical to base on the same host.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Benedikt Falk <bfalk@nvidia.com>
Session frontend for RFC vllm-project#51948, on top of the bounded-session core:

- POST /v1/streaming/sessions: open a session (model, prompt template,
  retention budgets, sampling); returns a session id.
- POST /v1/streaming/sessions/{id}/frames: push one chunk (video frame
  or text turn), block until that chunk's reply is complete, return it.
  One-chunk-in-flight per session by design: the producer paces the
  engine so replies stay aligned with the chunks they describe.
- DELETE /v1/streaming/sessions/{id}: close (also closed on
  client disconnect and engine error; fail-closed).

Gated behind --enable-streaming (off by default). Startup guards fail
fast on incompatible configs instead of dying at first use: multimodal
processor cache is forced off (evicted-frame content must never be
served from a data-less cache hit), async scheduling defaults off,
pipeline parallelism and multi-API-server configs are rejected (the
session registry is per-process).

Transport shape is a proposal: happy to converge with /v1/realtime
(vllm-project#33187) or the ingest path in vllm-project#50570 instead; the session layer is a
thin adapter over AsyncLLM.generate(stream) either way.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Benedikt Falk <bfalk@nvidia.com>
@mergify mergify Bot added frontend multi-modality Related to multi-modality (#4194) scheduler kv-cache-manager labels Aug 26, 2026
@nvbfalk nvbfalk changed the title Pr4 streaming session api [Frontend] Streaming session API (/v1/streaming) Aug 26, 2026
@mergify

mergify Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

This pull request has merge conflicts that must be resolved before it can be
merged. Please rebase the PR, @nvbfalk.

https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork

@mergify mergify Bot added the needs-rebase label Sep 9, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant