[FEAT][Fronted] Support incremental prefill for realtime video streaming (#5894)

Signed-off-by: ZhengWG <zwg0606@gmail.com>
Signed-off-by: Zheng Wengang <zwg0606@gmail.com>
Co-authored-by: amy-why-3459 <wuhaiyan17@huawei.com>
This commit is contained in:
Zheng Wengangandamy-why-3459 authored and GitHub committed 2026-10-02 22:18:51 +08:00
1 parent e372aa3a41
commit eb1699bfca
6 files changed
+1187 -234

No files matched your search

@@ -7,10 +7,12 @@ from __future__ import annotations
import asyncio
import base64
import gc
import hashlib
import io
import json
import threading
import weakref
from types import SimpleNamespace
from typing import Any
from unittest.mock import AsyncMock, MagicMock
@@ -26,7 +28,9 @@ from vllm_omni.entrypoints.openai.serving_video_stream import (
QwenOmniStreamingVideoHandler,
StreamingVideoSessionConfig,
)
from vllm_omni.entrypoints.openai.video_stream_base import OmniStreamingVideoHandler
from vllm_omni.entrypoints.openai.video_stream_base import (
OmniStreamingVideoHandler,
)
from vllm_omni.outputs import OmniRequestOutput
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
@@ -506,17 +510,9 @@ async def test_video_frames_consumed_excludes_bad_frame_in_query_snapshot(
pytest.param(2, ["f0", "f1", "f2"], True, [0, 2], id="repeated-image"),
],
)
async def test_video_frame_selection_preserves_uncached_frames(
monkeypatch, num_frames, frame_ids, repeat_image, expected_indices
):
"""Prewarm misses still use image URLs, with metadata paired by buffer position."""
async def test_video_frame_selection_pairs_metadata_by_position(num_frames, frame_ids, repeat_image, expected_indices):
"""Queries wait for prewarmed PIL frames, with metadata paired by buffer position."""
engine_prompts = []
pending_decode = asyncio.Event()
async def blocked_to_thread(*args, **kwargs):
await pending_decode.wait()
monkeypatch.setattr(asyncio, "to_thread", blocked_to_thread)
class OneOutputEngine:
async def generate(self, **kwargs):
@@ -550,13 +546,13 @@ async def test_video_frame_selection_preserves_uncached_frames(
assert not [message for message in ws.sent if message["type"] == "error"]
assert len(engine_prompts) == 1
content = engine_prompts[0]["messages"][-1]["content"]
assert content == [
*[
{"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{frames[index]}"}}
for index in expected_indices
],
{"type": "text", "text": "describe"},
]
assert content[-1] == {"type": "text", "text": "describe"}
image_parts = content[:-1]
assert [part["type"] for part in image_parts] == ["image_pil"] * len(expected_indices)
for part, index in zip(image_parts, expected_indices):
raw = base64.b64decode(frames[index])
assert part["image_pil"].tobytes() == Image.open(io.BytesIO(raw)).convert("RGB").tobytes()
assert part["uuid"] == hashlib.md5(raw, usedforsecurity=False).hexdigest()
consumed_events = [message for message in ws.sent if message["type"] == "video.frames.consumed"]
if not any(frame_ids):
assert consumed_events == []
@@ -909,6 +905,9 @@ async def test_interrupted_queries_wait_for_abort_without_fixed_delay(monkeypatc
class BlockingEngine:
async def generate(self, *, request_id, **kwargs):
submitted = kwargs.get("submitted")
if submitted is not None and not submitted.done():
submitted.set_result(None)
if request_ids:
assert aborted[-1] == request_ids[-1]
request_ids.append(request_id)
@@ -1004,7 +1003,7 @@ async def test_video_done_waits_for_in_flight_query():
@pytest.mark.asyncio
async def test_frame_prewarm_does_not_block_following_query(monkeypatch):
async def test_query_waits_for_frame_prewarm(monkeypatch):
decode_started = threading.Event()
release_decode = threading.Event()
query_started = asyncio.Event()
@@ -1035,14 +1034,80 @@ async def test_frame_prewarm_does_not_block_following_query(monkeypatch):
assert decode_started.is_set()
ws.put({"type": "video.query", "text": "describe"})
await asyncio.wait_for(query_started.wait(), timeout=2.0)
await asyncio.sleep(0.05)
assert not query_started.is_set()
release_decode.set()
await asyncio.wait_for(query_started.wait(), timeout=2.0)
ws.put({"type": "video.done"})
await asyncio.wait_for(task, timeout=2.0)
assert "session.done" in ws.sent_types()
@pytest.mark.asyncio
async def test_legacy_query_does_not_wait_for_unselected_frame(monkeypatch):
"""APC off and num_frames=1: a blocked older frame must not delay the query."""
frame_a = _make_jpeg(1, 0, 0)
frame_b = _make_jpeg(0, 1, 0)
decode_a_started = threading.Event()
release_a = threading.Event()
decode_b_done = threading.Event()
query_started = asyncio.Event()
def gated_decode(raw_bytes: bytes):
if raw_bytes == frame_a:
decode_a_started.set()
release_a.wait(timeout=5.0)
image = Image.open(io.BytesIO(raw_bytes)).convert("RGB")
if raw_bytes == frame_b:
decode_b_done.set()
return image
class RecordingHandler(QwenOmniStreamingVideoHandler):
async def _process_query(self, *args, **kwargs):
query_started.set()
monkeypatch.setattr(video_stream_base, "_decode_frame_bytes", gated_decode)
ws = TimedWebSocket()
handler = RecordingHandler(chat_service=object(), engine_client=object(), idle_timeout=5.0)
task = asyncio.create_task(handler.handle_session(ws))
try:
ws.put(
{
"type": "session.config",
"model": "test",
"modalities": ["text"],
"num_frames": 1,
"enable_frame_filter": False,
}
)
await asyncio.sleep(0)
ws.put({"type": "video.frame", "data": _b64(frame_a), "frame_id": "A"})
for _ in range(100):
if decode_a_started.is_set():
break
await asyncio.sleep(0.01)
assert decode_a_started.is_set()
ws.put({"type": "video.frame", "data": _b64(frame_b), "frame_id": "B"})
for _ in range(200):
if decode_b_done.is_set():
break
await asyncio.sleep(0.01)
assert decode_b_done.is_set()
ws.put({"type": "video.query", "text": "describe"})
await asyncio.wait_for(query_started.wait(), timeout=2.0)
assert not release_a.is_set()
release_a.set()
ws.put({"type": "video.done"})
await asyncio.wait_for(task, timeout=2.0)
finally:
release_a.set()
if not task.done():
task.cancel()
await asyncio.gather(task, return_exceptions=True)
@pytest.mark.asyncio
@pytest.mark.parametrize("max_frames", [1, 2], ids=["evicted", "retained"])
async def test_frame_prewarm_only_keeps_retained_images(monkeypatch, max_frames):
@@ -1272,3 +1337,452 @@ def test_build_messages_keeps_recent_history_text_only():
assert messages[1] == {"role": "assistant", "content": "recent answer"}
assert messages[2] == user_message
assert user_message["content"][-1] == {"type": "text", "text": "current question"}
# ---------------------------------------------------------------------------
# Incremental prefill (warmup) lifecycle
# ---------------------------------------------------------------------------
class RecordingReuseEngine:
"""Engine stub with stage-0 prefix caching on; records generate() and abort()."""
def __init__(self, warmup_delay: float = 0.0, query_delay: float = 0.0):
self.requests: list[dict[str, Any]] = []
self.aborted: list[str] = []
self._warmup_delay = warmup_delay
self._query_delay = query_delay
self.engine = SimpleNamespace(
stage_vllm_configs=[SimpleNamespace(cache_config=SimpleNamespace(enable_prefix_caching=True))]
)
def warmup_ids(self) -> list[str]:
return [r["request_id"] for r in self.requests if r["request_id"].startswith("video-warmup-")]
def query_ids(self) -> list[str]:
return [r["request_id"] for r in self.requests if not r["request_id"].startswith("video-warmup-")]
def generate(self, **kwargs):
self.requests.append(kwargs)
submitted = kwargs.get("submitted")
if submitted is not None and not submitted.done():
submitted.set_result(None)
is_warmup = kwargs["request_id"].startswith("video-warmup-")
delay = self._warmup_delay if is_warmup else self._query_delay
async def _gen():
if delay:
await asyncio.sleep(delay)
yield _text_result("ok")
return _gen()
async def abort(self, request_id: str) -> None:
self.aborted.append(request_id)
class PromptRecordingHandler(QwenOmniStreamingVideoHandler):
"""Records each query turn's user-message content and processor kwargs."""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.query_contents: list[list[dict[str, Any]]] = []
self.warmup_contents: list[list[dict[str, Any]]] = []
self.mm_processor_kwargs: list[Any] = []
async def _preprocess_to_engine_prompt(self, request):
self.mm_processor_kwargs.append(getattr(request, "mm_processor_kwargs", None))
return {"prompt": "p"}
def build_engine_prompt(
self, config, frame_buffer, audio_buffer, message_history, query_text, prewarmed_frames, *, frame_indices=None
):
messages, user_message = super().build_engine_prompt(
config,
frame_buffer,
audio_buffer,
message_history,
query_text,
prewarmed_frames,
frame_indices=frame_indices,
)
self.query_contents.append(list(user_message["content"]))
return messages, user_message
def build_engine_prompt_prefix(self, config, frame_buffer, message_history, prewarmed_frames):
messages = super().build_engine_prompt_prefix(config, frame_buffer, message_history, prewarmed_frames)
if messages:
self.warmup_contents.append(list(messages[-1]["content"]))
return messages
async def _poll(predicate, timeout: float = 3.0) -> bool:
for _ in range(int(timeout / 0.01)):
if predicate():
return True
await asyncio.sleep(0.01)
return bool(predicate())
@pytest.mark.asyncio
async def test_warmup_fires_on_frame_and_query_cancels_it():
engine = RecordingReuseEngine(warmup_delay=10.0, query_delay=0.3)
handler = PromptRecordingHandler(chat_service=object(), engine_client=engine, idle_timeout=5.0)
ws = TimedWebSocket()
task = asyncio.create_task(handler.handle_session(ws))
ws.put({"type": "session.config", "model": "test", "modalities": ["text"], "enable_frame_filter": False})
await asyncio.sleep(0)
ws.put({"type": "video.frame", "data": _b64(_make_jpeg())})
assert await _poll(lambda: engine.warmup_ids())
warmup_id = engine.warmup_ids()[0]
assert engine.requests[0]["sampling_params"].max_tokens == 1
ws.put({"type": "video.query", "text": "describe"})
assert await _poll(lambda: engine.query_ids())
# in-flight warmup was cancelled + aborted, and not restarted mid-query
assert warmup_id in engine.aborted
assert len(engine.warmup_ids()) == 1
assert handler.mm_processor_kwargs[:2] == [
{"use_audio_in_video": True},
{"use_audio_in_video": True},
]
assert await _poll(lambda: "response.text.done" in ws.sent_types())
# the committed turn changed the context -> a fresh warmup fires
assert await _poll(lambda: len(engine.warmup_ids()) == 2)
ws.put({"type": "video.done"})
await asyncio.wait_for(task, timeout=2.0)
@pytest.mark.asyncio
async def test_cancelled_warmup_reaches_stage0_before_abort():
"""Cancel during submit must not abort a warmup the engine never received."""
class _HoldSubmitEngine(RecordingReuseEngine):
def __init__(self):
super().__init__()
self.started = asyncio.Event()
self.release_submit = asyncio.Event()
def generate(self, **kwargs):
submitted = kwargs.get("submitted")
async def _gen():
self.started.set()
await self.release_submit.wait()
self.requests.append(kwargs)
if submitted is not None and not submitted.done():
submitted.set_result(None)
yield _text_result("ok")
return _gen()
engine = _HoldSubmitEngine()
handler = PromptRecordingHandler(chat_service=object(), engine_client=engine, idle_timeout=5.0)
ws = TimedWebSocket()
task = asyncio.create_task(handler.handle_session(ws))
ws.put({"type": "session.config", "model": "test", "modalities": ["text"], "enable_frame_filter": False})
await asyncio.sleep(0)
ws.put({"type": "video.frame", "data": _b64(_make_jpeg())})
assert await _poll(engine.started.is_set)
ws.put({"type": "video.query", "text": "describe"})
await asyncio.sleep(0.05)
assert engine.requests == []
assert engine.aborted == []
engine.release_submit.set()
assert await _poll(lambda: engine.aborted)
assert engine.warmup_ids()
assert engine.aborted[0] == engine.warmup_ids()[0]
assert await _poll(lambda: engine.query_ids())
ws.put({"type": "video.done"})
await asyncio.wait_for(task, timeout=2.0)
@pytest.mark.asyncio
async def test_cancelled_query_reaches_stage0_before_abort():
"""Cancel during query submit must not abort a request the engine never received."""
class _HoldQueryEngine(RecordingReuseEngine):
def __init__(self):
super().__init__()
self.query_started = asyncio.Event()
self.release_submit = asyncio.Event()
def generate(self, **kwargs):
if str(kwargs.get("request_id", "")).startswith("video-warmup-"):
return super().generate(**kwargs)
submitted = kwargs.get("submitted")
async def _gen():
self.query_started.set()
await self.release_submit.wait()
self.requests.append(kwargs)
if submitted is not None and not submitted.done():
submitted.set_result(None)
yield _text_result("ok")
return _gen()
engine = _HoldQueryEngine()
handler = PromptRecordingHandler(chat_service=object(), engine_client=engine, idle_timeout=5.0)
ws = TimedWebSocket()
task = asyncio.create_task(handler.handle_session(ws))
ws.put({"type": "session.config", "model": "test", "modalities": ["text"], "enable_frame_filter": False})
await asyncio.sleep(0)
ws.put({"type": "video.frame", "data": _b64(_make_jpeg())})
assert await _poll(lambda: engine.warmup_ids())
ws.put({"type": "video.query", "text": "first"})
assert await _poll(engine.query_started.is_set)
ws.put({"type": "video.query", "text": "second"})
await asyncio.sleep(0.05)
assert engine.query_ids() == []
assert engine.aborted == []
engine.release_submit.set()
assert await _poll(lambda: engine.query_ids())
first_query = engine.query_ids()[0]
assert await _poll(lambda: first_query in engine.aborted)
assert await _poll(lambda: len(engine.query_ids()) == 2)
ws.put({"type": "video.done"})
await asyncio.wait_for(task, timeout=2.0)
@pytest.mark.asyncio
async def test_audio_session_never_starts_warmup():
engine = RecordingReuseEngine()
handler = PromptRecordingHandler(chat_service=object(), engine_client=engine, idle_timeout=5.0)
ws = TimedWebSocket()
task = asyncio.create_task(handler.handle_session(ws))
ws.put({"type": "session.config", "model": "test", "modalities": ["text", "audio"], "enable_frame_filter": False})
await asyncio.sleep(0)
ws.put({"type": "video.frame", "data": _b64(_make_jpeg())})
ws.put({"type": "video.frame", "data": _b64(_make_jpeg(9, 9, 9))})
await asyncio.sleep(0.3)
assert engine.warmup_ids() == []
ws.put({"type": "video.done"})
await asyncio.wait_for(task, timeout=2.0)
@pytest.mark.asyncio
async def test_warmup_starts_only_after_frames_ready(monkeypatch):
decode_started = threading.Event()
release_decode = threading.Event()
def gated_decode(raw_bytes: bytes):
decode_started.set()
release_decode.wait(timeout=5.0)
return Image.open(io.BytesIO(raw_bytes)).convert("RGB")
monkeypatch.setattr(video_stream_base, "_decode_frame_bytes", gated_decode)
engine = RecordingReuseEngine()
handler = PromptRecordingHandler(chat_service=object(), engine_client=engine, idle_timeout=5.0)
ws = TimedWebSocket()
task = asyncio.create_task(handler.handle_session(ws))
ws.put({"type": "session.config", "model": "test", "modalities": ["text"], "enable_frame_filter": False})
await asyncio.sleep(0)
ws.put({"type": "video.frame", "data": _b64(_make_jpeg())})
assert await _poll(decode_started.is_set)
await asyncio.sleep(0.1)
assert engine.warmup_ids() == []
release_decode.set()
assert await _poll(lambda: engine.warmup_ids())
ws.put({"type": "video.done"})
await asyncio.wait_for(task, timeout=2.0)
@pytest.mark.asyncio
async def test_query_cancelling_warmup_keeps_prewarm_alive(monkeypatch):
"""Cancel warmup during prefill must not drop a later frame still decoding."""
frame1 = _b64(_make_jpeg(1, 1, 1))
frame2 = _b64(_make_jpeg(2, 2, 2))
raw2 = base64.b64decode(frame2)
decode_started = threading.Event()
release_decode = threading.Event()
def gated_decode(raw_bytes: bytes):
if raw_bytes == raw2:
decode_started.set()
release_decode.wait(timeout=5.0)
return Image.open(io.BytesIO(raw_bytes)).convert("RGB")
monkeypatch.setattr(video_stream_base, "_decode_frame_bytes", gated_decode)
engine = RecordingReuseEngine(warmup_delay=10.0)
handler = PromptRecordingHandler(chat_service=object(), engine_client=engine, idle_timeout=5.0)
ws = TimedWebSocket()
task = asyncio.create_task(handler.handle_session(ws))
ws.put({"type": "session.config", "model": "test", "modalities": ["text"], "enable_frame_filter": False})
await asyncio.sleep(0)
ws.put({"type": "video.frame", "data": frame1})
assert await _poll(lambda: engine.warmup_ids())
ws.put({"type": "video.frame", "data": frame2})
assert await _poll(decode_started.is_set)
ws.put({"type": "video.query", "text": "describe"})
await asyncio.sleep(0.1)
release_decode.set()
assert await _poll(lambda: "response.text.done" in ws.sent_types())
assert len(handler.query_contents) == 1
image_parts = [p for p in handler.query_contents[0] if p.get("type") == "image_pil"]
assert len(image_parts) == 2
ws.put({"type": "video.done"})
await asyncio.wait_for(task, timeout=2.0)
@pytest.mark.asyncio
async def test_query_keeps_frames_evicted_while_pinned(monkeypatch):
"""Regression: frames FIFO-evicted while an in-flight query awaits prewarm
must keep their PIL entries and stay in the query prompt."""
frame_a = _b64(_make_jpeg(1, 1, 1))
frame_a2 = _b64(_make_jpeg(2, 2, 2))
frame_b = _b64(_make_jpeg(3, 3, 3))
raw_a = base64.b64decode(frame_a)
decoded: list[bytes] = []
release_others = threading.Event()
def gated_decode(raw_bytes: bytes):
if raw_bytes != raw_a:
release_others.wait(timeout=5.0)
decoded.append(raw_bytes)
return Image.open(io.BytesIO(raw_bytes)).convert("RGB")
monkeypatch.setattr(video_stream_base, "_decode_frame_bytes", gated_decode)
engine = RecordingReuseEngine()
handler = PromptRecordingHandler(chat_service=object(), engine_client=engine, idle_timeout=5.0)
ws = TimedWebSocket()
task = asyncio.create_task(handler.handle_session(ws))
ws.put(
{
"type": "session.config",
"model": "test",
"modalities": ["text"],
"max_frames": 2,
"enable_frame_filter": False,
}
)
await asyncio.sleep(0)
ws.put({"type": "video.frame", "data": frame_a})
assert await _poll(lambda: raw_a in decoded)
ws.put({"type": "video.frame", "data": frame_a2}) # decode gated -> query will wait
ws.put({"type": "video.query", "text": "describe"}) # pins [a, a2]
await asyncio.sleep(0.1)
ws.put({"type": "video.frame", "data": frame_b}) # buffer full -> evicts pinned a
await asyncio.sleep(0.1)
release_others.set()
assert await _poll(lambda: "response.text.done" in ws.sent_types())
assert len(handler.query_contents) == 1
uuids = [p["uuid"] for p in handler.query_contents[0] if p.get("type") == "image_pil"]
expected = [hashlib.md5(base64.b64decode(f), usedforsecurity=False).hexdigest() for f in (frame_a, frame_a2)]
assert uuids == expected
ws.put({"type": "video.done"})
await asyncio.wait_for(task, timeout=2.0)
async def _query_image_uuids(modalities: list[str], frames: list[str], *, max_frames: int) -> list[str]:
engine = RecordingReuseEngine()
handler = PromptRecordingHandler(chat_service=object(), engine_client=engine, idle_timeout=5.0)
ws = TimedWebSocket()
task = asyncio.create_task(handler.handle_session(ws))
ws.put(
{
"type": "session.config",
"model": "test",
"modalities": modalities,
"max_frames": max_frames,
"num_frames": max_frames,
"enable_frame_filter": False,
}
)
await asyncio.sleep(0)
for frame in frames:
ws.put({"type": "video.frame", "data": frame})
ws.put({"type": "video.query", "text": "describe"})
assert await _poll(lambda: handler.query_contents)
ws.put({"type": "video.done"})
await asyncio.wait_for(task, timeout=2.0)
return [part["uuid"] for part in handler.query_contents[0] if part.get("type") == "image_pil"]
def _frame_uuid(frame_b64: str) -> str:
return hashlib.md5(base64.b64decode(frame_b64), usedforsecurity=False).hexdigest()
@pytest.mark.asyncio
async def test_below_max_frames_incremental_matches_legacy_window():
frames = [_b64(_make_jpeg(i, 0, 0)) for i in range(3)]
expected = [_frame_uuid(frame) for frame in frames]
assert await _query_image_uuids(["text"], frames, max_frames=4) == expected
assert await _query_image_uuids(["text", "audio"], frames, max_frames=4) == expected
@pytest.mark.asyncio
async def test_incremental_full_window_drops_oldest_half():
frames = [_b64(_make_jpeg(i, 1, 0)) for i in range(5)]
kept = await _query_image_uuids(["text"], frames, max_frames=4)
legacy = await _query_image_uuids(["text", "audio"], frames, max_frames=4)
assert kept == [_frame_uuid(frame) for frame in frames[2:]]
assert legacy == [_frame_uuid(frame) for frame in frames[1:]]
async def _run_frame_audio_query(modalities: list[str], frame: str, pcm: bytes) -> PromptRecordingHandler:
engine = RecordingReuseEngine()
handler = PromptRecordingHandler(chat_service=object(), engine_client=engine, idle_timeout=5.0)
ws = TimedWebSocket()
task = asyncio.create_task(handler.handle_session(ws))
ws.put(
{
"type": "session.config",
"model": "test",
"modalities": modalities,
"enable_frame_filter": False,
}
)
await asyncio.sleep(0)
ws.put({"type": "video.frame", "data": frame})
if "audio" not in modalities:
assert await _poll(lambda: engine.warmup_ids())
else:
await asyncio.sleep(0.05)
assert engine.warmup_ids() == []
ws.put({"type": "audio.chunk", "data": _b64(pcm)})
ws.put({"type": "video.query", "text": "describe"})
assert await _poll(lambda: engine.query_ids())
ws.put({"type": "video.done"})
await asyncio.wait_for(task, timeout=2.0)
return handler
@pytest.mark.asyncio
async def test_reuse_query_with_audio_matches_legacy_prompt():
"""Incremental and legacy query prompts match; both set the processor kwarg."""
frame = _b64(_make_jpeg())
pcm = b"\x01\x00\x02\x00"
reuse = await _run_frame_audio_query(["text"], frame, pcm)
legacy = await _run_frame_audio_query(["text", "audio"], frame, pcm)
assert reuse.warmup_contents
assert all(part.get("type") != "input_audio" for part in reuse.warmup_contents[0])
assert [part.get("type") for part in reuse.warmup_contents[0]] == ["image_pil"]
assert reuse.query_contents and legacy.query_contents
reuse_q = reuse.query_contents[0]
legacy_q = legacy.query_contents[0]
assert [part.get("type") for part in reuse_q] == ["image_pil", "input_audio", "text"]
assert [part.get("type") for part in legacy_q] == ["image_pil", "input_audio", "text"]
assert reuse_q[0]["uuid"] == legacy_q[0]["uuid"]
assert reuse_q[1] == legacy_q[1]
assert reuse_q[2] == legacy_q[2] == {"type": "text", "text": "describe"}
assert reuse.mm_processor_kwargs[0] == {"use_audio_in_video": True}
assert reuse.mm_processor_kwargs[1] == {"use_audio_in_video": True}
assert legacy.mm_processor_kwargs[0] == {"use_audio_in_video": True}
@@ -174,7 +174,9 @@ async def test_real_producer_deltas_reach_video_client(
produced.append(OmniRequestOutput.from_stage_output(result, final_output_type="audio"))
class Engine:
async def generate(self, *, prompt, request_id, output_modalities, sampling_params_list=None):
async def generate(self, *, prompt, request_id, output_modalities, sampling_params_list=None, submitted=None):
if submitted is not None and not submitted.done():
submitted.set_result(None)
if explicit_sampling:
assert sampling_params_list is not None
assert sampling_params_list[0].output_kind == RequestOutputKind.DELTA