Signed-off-by: Yueqian Lin <linyueqian@outlook.com> Co-authored-by: Hongsheng Liu <liuhongsheng4@huawei.com>
This commit is contained in:
1 parent
8d83fb59e9
commit
c067485030
7 files changed
+240
-5
No files matched your search
@@ -447,6 +447,7 @@ python qwen3_tts/gradio_fastrtc_demo.py --api-base http://localhost:8000
|
||||
- Base voice cloning has uniproc-vs-mp tradeoffs depending on per-request reference audio cost; see the executor-backend section above.
|
||||
- With async chunking, Qwen3-TTS Base voice cloning sends the full reference context in the first Code2Wav packet, then caches that prefix on the Code2Wav stage for follow-up chunks in the same request.
|
||||
- `vllm_omni/deploy/qwen3_tts.yaml` is the default deploy config (loaded by HF `model_type`); per-stage runtime overrides are available via `--stage-N-<field> <value>`.
|
||||
- Under vocoder-bound overload (single-stream `rtf_p99 ≥ 1` at the target concurrency), set `active_stream_window: 2` at the top of the deploy yaml to cap simultaneously active Stage 1 streams. Off by default; trades TTFP for streaming continuity. See [#3592](https://github.com/vllm-project/vllm-omni/pull/3592) for the mechanism and tradeoff numbers.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -50,6 +50,7 @@ def build_adapter(monkeypatch, mocker: MockerFixture):
|
||||
stage_id: int = 1,
|
||||
model_mode: str = "ar",
|
||||
max_num_seqs: int = 2,
|
||||
active_stream_window: int = 0,
|
||||
connector_extra: dict | None = None,
|
||||
):
|
||||
connector = mocker.MagicMock()
|
||||
@@ -76,7 +77,11 @@ def build_adapter(monkeypatch, mocker: MockerFixture):
|
||||
classmethod(lambda cls, _model_config: connector),
|
||||
)
|
||||
|
||||
model_config = SimpleNamespace(worker_type=model_mode)
|
||||
model_config = SimpleNamespace(
|
||||
worker_type=model_mode,
|
||||
max_num_seqs=max_num_seqs,
|
||||
active_stream_window=active_stream_window,
|
||||
)
|
||||
scheduler_config = SimpleNamespace(max_num_seqs=max_num_seqs)
|
||||
adapter = OmniChunkTransferAdapter(
|
||||
SimpleNamespace(model_config=model_config, scheduler_config=scheduler_config)
|
||||
@@ -396,6 +401,75 @@ def test_process_and_restore_queues(build_adapter):
|
||||
assert adapter.waiting_for_chunk_running_requests == deque()
|
||||
|
||||
|
||||
def test_fifo_promotion(build_adapter):
|
||||
adapter, _ = build_adapter(stage_id=1, max_num_seqs=2, active_stream_window=2)
|
||||
reqs = [_req(f"req-{idx}", RequestStatus.WAITING) for idx in range(1, 5)]
|
||||
waiting_queue = DummyWaitingQueue(reqs)
|
||||
running_queue = []
|
||||
|
||||
adapter.process_pending_chunks(waiting_queue, running_queue)
|
||||
|
||||
assert list(adapter._active_streams) == ["req-1", "req-2"]
|
||||
assert waiting_queue == reqs[2:]
|
||||
assert reqs[0].status == RequestStatus.WAITING_FOR_CHUNK
|
||||
assert reqs[1].status == RequestStatus.WAITING_FOR_CHUNK
|
||||
assert reqs[2].status == RequestStatus.WAITING
|
||||
|
||||
adapter.finished_requests.add("req-1")
|
||||
# Eviction is deferred to postprocess_scheduler_output in the runtime path
|
||||
# (commit c4d95fd9 — otherwise the terminal chunk deadlocks at c=8 K=2).
|
||||
# Simulate it here so promotion can pick up the freed slot.
|
||||
adapter._evict_finished_active_streams({"req-1"})
|
||||
adapter.process_pending_chunks(waiting_queue, running_queue)
|
||||
|
||||
assert list(adapter._active_streams) == ["req-2", "req-3"]
|
||||
# Promotion + chunk processing happen in the same call, so req-3 is
|
||||
# already WAITING_FOR_CHUNK by the time we check.
|
||||
assert reqs[2].status == RequestStatus.WAITING_FOR_CHUNK
|
||||
|
||||
|
||||
def test_legacy_k0(build_adapter):
|
||||
adapter, _ = build_adapter(stage_id=1, max_num_seqs=1, active_stream_window=0)
|
||||
waiting_req = _req("waiting", RequestStatus.WAITING)
|
||||
running_req_1 = _req("running-1", RequestStatus.RUNNING)
|
||||
running_req_2 = _req("running-2", RequestStatus.RUNNING)
|
||||
waiting_queue = DummyWaitingQueue([waiting_req])
|
||||
running_queue = [running_req_1, running_req_2]
|
||||
|
||||
adapter.requests_with_ready_chunks.update({"running-1", "running-2"})
|
||||
adapter.process_pending_chunks(waiting_queue, running_queue)
|
||||
|
||||
assert waiting_req.status == RequestStatus.WAITING_FOR_CHUNK
|
||||
assert adapter.waiting_for_chunk_waiting_requests == deque([waiting_req])
|
||||
assert running_queue == [running_req_1]
|
||||
assert waiting_queue == [running_req_2]
|
||||
assert running_req_2.status == RequestStatus.PREEMPTED
|
||||
assert adapter._active_streams == {}
|
||||
|
||||
|
||||
def test_finished_releases_slot(build_adapter):
|
||||
adapter, _ = build_adapter(stage_id=1, max_num_seqs=1, active_stream_window=1)
|
||||
req_1 = _req("req-1", RequestStatus.WAITING)
|
||||
req_2 = _req("req-2", RequestStatus.WAITING)
|
||||
waiting_queue = DummyWaitingQueue([req_1, req_2])
|
||||
running_queue = []
|
||||
|
||||
adapter.process_pending_chunks(waiting_queue, running_queue)
|
||||
assert list(adapter._active_streams) == ["req-1"]
|
||||
assert waiting_queue == [req_2]
|
||||
|
||||
adapter.finished_requests.add("req-1")
|
||||
# Eviction is deferred to postprocess_scheduler_output in the runtime path
|
||||
# (commit c4d95fd9). Simulate it so promotion can pick up the freed slot.
|
||||
adapter._evict_finished_active_streams({"req-1"})
|
||||
adapter.process_pending_chunks(waiting_queue, running_queue)
|
||||
|
||||
assert list(adapter._active_streams) == ["req-2"]
|
||||
# Promotion + chunk processing happen in the same call, so req-2 is
|
||||
# already WAITING_FOR_CHUNK by the time we check.
|
||||
assert req_2.status == RequestStatus.WAITING_FOR_CHUNK
|
||||
|
||||
|
||||
def test_postprocess_scheduler_output(build_adapter):
|
||||
adapter, _ = build_adapter()
|
||||
adapter.requests_with_ready_chunks = {"new-ready", "cached-ready", "leftover"}
|
||||
@@ -422,6 +496,7 @@ def test_postprocess_scheduler_output(build_adapter):
|
||||
def _populate_adapter_state(adapter, req_id="req-1", ext_id="ext-1"):
|
||||
"""Fill every per-request structure so cleanup can be verified."""
|
||||
adapter.finished_requests.add(req_id)
|
||||
adapter._active_streams[req_id] = SimpleNamespace(request_id=req_id)
|
||||
adapter.get_req_chunk[req_id] = 3
|
||||
adapter.requests_with_ready_chunks.add(req_id)
|
||||
adapter.request_ids_mapping[req_id] = ext_id
|
||||
@@ -442,6 +517,7 @@ def test_cleanup_clears_all_state(build_adapter):
|
||||
adapter.cleanup(req_id, ext_id)
|
||||
|
||||
assert req_id not in adapter.finished_requests
|
||||
assert req_id not in adapter._active_streams
|
||||
assert req_id not in adapter.get_req_chunk
|
||||
assert req_id not in adapter.requests_with_ready_chunks
|
||||
assert req_id not in adapter.request_ids_mapping
|
||||
|
||||
@@ -1020,6 +1020,7 @@ class TestPipelineRegistry:
|
||||
class TestDeployConfigLoading:
|
||||
def test_deploy_override_fields_include_deploy_schema_fields(self):
|
||||
expected_fields = {
|
||||
"active_stream_window",
|
||||
"async_chunk",
|
||||
# StageDeployConfig: stage placement and runtime fields.
|
||||
"devices",
|
||||
|
||||
@@ -119,6 +119,8 @@ class OmniModelConfig(ModelConfig):
|
||||
|
||||
stage_id: int = 0
|
||||
async_chunk: bool = False
|
||||
# Stage-1 active stream slots; 0 keeps legacy chunk-level round-robin.
|
||||
active_stream_window: int = 0
|
||||
model_stage: str = "thinker"
|
||||
model_arch: str | None = None
|
||||
worker_type: str | None = None
|
||||
|
||||
@@ -500,6 +500,8 @@ class DeployConfig:
|
||||
"""
|
||||
|
||||
async_chunk: bool = True
|
||||
# Stage-1 active stream slots; 0 preserves legacy all-stream cycling.
|
||||
active_stream_window: int = 0
|
||||
connectors: dict[str, Any] | None = None
|
||||
edges: list[dict[str, Any]] | None = None
|
||||
stages: list[StageDeployConfig] = field(default_factory=list)
|
||||
@@ -677,6 +679,7 @@ def load_deploy_config(path: str | Path) -> DeployConfig:
|
||||
|
||||
kwargs: dict[str, Any] = {
|
||||
"async_chunk": raw_dict.get("async_chunk", True),
|
||||
"active_stream_window": int(raw_dict.get("active_stream_window", 0) or 0),
|
||||
"connectors": raw_dict.get("connectors", None),
|
||||
"edges": raw_dict.get("edges", None),
|
||||
"stages": stages,
|
||||
@@ -816,6 +819,7 @@ _PIPELINE_WIDE_ENGINE_FIELDS: tuple[str, ...] = (
|
||||
"enable_chunked_prefill",
|
||||
"data_parallel_size",
|
||||
"pipeline_parallel_size",
|
||||
"active_stream_window",
|
||||
"custom_voice_dir",
|
||||
)
|
||||
|
||||
|
||||
@@ -41,6 +41,20 @@ class OmniChunkTransferAdapter(OmniTransferAdapterBase):
|
||||
def __init__(self, vllm_config: Any):
|
||||
model_config = vllm_config.model_config
|
||||
self.scheduler_max_num_seqs = vllm_config.scheduler_config.max_num_seqs
|
||||
active_stream_window = int(getattr(model_config, "active_stream_window", 0) or 0)
|
||||
model_max_num_seqs = int(getattr(model_config, "max_num_seqs", self.scheduler_max_num_seqs) or 0)
|
||||
if model_max_num_seqs <= 0:
|
||||
model_max_num_seqs = self.scheduler_max_num_seqs
|
||||
self._active_window = min(active_stream_window, model_max_num_seqs) if active_stream_window > 0 else 0
|
||||
if self._active_window > 0:
|
||||
logger.info(
|
||||
"Bounded active-stream window enabled: K=%d. "
|
||||
"Multi-replica deployments require sticky per-stream routing across Stage 1 "
|
||||
"replicas (each replica owns an independent active-set; without sticky routing, "
|
||||
"a stream can be active on one replica and non-active on another and both will "
|
||||
"race to evict it).",
|
||||
self._active_window,
|
||||
)
|
||||
self.connector = self.create_connector(model_config)
|
||||
super().__init__(model_config)
|
||||
self.model_mode = getattr(model_config, "worker_type", None) or "ar"
|
||||
@@ -64,6 +78,14 @@ class OmniChunkTransferAdapter(OmniTransferAdapterBase):
|
||||
self.waiting_for_chunk_running_requests: deque[Any] = deque()
|
||||
self.requests_with_ready_chunks = set()
|
||||
self.requests_origin_status = {}
|
||||
self._active_streams: dict[str, Any] = {}
|
||||
# Private hold-queue for non-active running requests. Restored to
|
||||
# running_queue inside restore_queues(). Avoids calling
|
||||
# waiting_queue.prepend_requests mid-step, which trips vllm's
|
||||
# per-step LogitsProcessor invariant
|
||||
# ("Cannot register new removed request after self.removed has
|
||||
# been read").
|
||||
self._held_non_active: deque[Any] = deque()
|
||||
self.requests_num_chunks_sent: dict[str, int] = defaultdict(int)
|
||||
self._pending_streaming_prefills: dict[str, dict] = {}
|
||||
|
||||
@@ -362,6 +384,10 @@ class OmniChunkTransferAdapter(OmniTransferAdapterBase):
|
||||
|
||||
Idempotent: calling with an already-cleaned or unknown id is safe.
|
||||
"""
|
||||
if request_id in self.finished_requests:
|
||||
self._evict_finished_active_streams({request_id})
|
||||
else:
|
||||
self._active_streams.pop(request_id, None)
|
||||
self.finished_requests.discard(request_id)
|
||||
self.segment_finished_requests.discard(request_id)
|
||||
self.get_req_chunk.pop(request_id, None)
|
||||
@@ -424,16 +450,112 @@ class OmniChunkTransferAdapter(OmniTransferAdapterBase):
|
||||
"""
|
||||
if self.connector.stage_id == 0:
|
||||
return
|
||||
if self._active_window <= 0:
|
||||
self._process_chunk_queue_legacy(
|
||||
waiting_queue, self.waiting_for_chunk_waiting_requests, RequestStatus.WAITING, self._finished_load_reqs
|
||||
)
|
||||
self._process_chunk_queue_legacy(
|
||||
running_queue,
|
||||
self.waiting_for_chunk_running_requests,
|
||||
RequestStatus.RUNNING,
|
||||
self._finished_load_reqs,
|
||||
)
|
||||
while len(running_queue) > self.scheduler_max_num_seqs:
|
||||
request = running_queue.pop()
|
||||
request.status = RequestStatus.PREEMPTED
|
||||
waiting_queue.prepend_requests([request])
|
||||
return
|
||||
|
||||
self._promote_active_streams(running_queue)
|
||||
self._promote_active_streams(waiting_queue)
|
||||
self._process_chunk_queue(
|
||||
waiting_queue, self.waiting_for_chunk_waiting_requests, RequestStatus.WAITING, self._finished_load_reqs
|
||||
)
|
||||
self._process_chunk_queue(
|
||||
running_queue, self.waiting_for_chunk_running_requests, RequestStatus.RUNNING, self._finished_load_reqs
|
||||
)
|
||||
while len(running_queue) > self.scheduler_max_num_seqs:
|
||||
request = running_queue.pop()
|
||||
request.status = RequestStatus.PREEMPTED
|
||||
waiting_queue.prepend_requests([request])
|
||||
self._promote_active_streams(waiting_queue)
|
||||
self._preempt_non_active_running(waiting_queue, running_queue)
|
||||
|
||||
def _evict_finished_active_streams(self, request_ids: set[str] | None = None) -> None:
|
||||
for request_id in list(self._active_streams):
|
||||
if request_ids is not None and request_id not in request_ids:
|
||||
continue
|
||||
if request_id in self.finished_requests:
|
||||
self._active_streams.pop(request_id, None)
|
||||
|
||||
def _promote_active_streams(self, queue: Any) -> None:
|
||||
if len(self._active_streams) >= self._active_window:
|
||||
return
|
||||
for request in list(queue):
|
||||
if len(self._active_streams) >= self._active_window:
|
||||
return
|
||||
request_id = request.request_id
|
||||
if request_id in self._active_streams or request_id in self.finished_requests:
|
||||
continue
|
||||
# Iterating the existing queue preserves FIFO admission.
|
||||
self._active_streams[request_id] = request
|
||||
|
||||
def _ensure_active_stream(self, request: Request) -> bool:
|
||||
if self._active_window <= 0:
|
||||
return True
|
||||
request_id = request.request_id
|
||||
if request_id in self._active_streams:
|
||||
self._active_streams[request_id] = request
|
||||
return True
|
||||
if request_id in self.finished_requests or len(self._active_streams) >= self._active_window:
|
||||
return False
|
||||
self._active_streams[request_id] = request
|
||||
return True
|
||||
|
||||
def _preempt_non_active_running(self, waiting_queue: Any, running_queue: list[Request]) -> None:
|
||||
# Hold non-active running requests in a private deque rather than
|
||||
# routing them back through waiting_queue. Routing through the
|
||||
# vllm RequestQueue mid-step triggers
|
||||
# "Cannot register new removed request after self.removed has
|
||||
# been read"
|
||||
# in vllm.v1.sample.logits_processor.state when the persistent
|
||||
# batch was already snapshotted. They are returned to
|
||||
# running_queue in restore_queues() so the next scheduler tick
|
||||
# re-evaluates them through _promote_active_streams.
|
||||
index = len(running_queue) - 1
|
||||
while index >= 0:
|
||||
request = running_queue[index]
|
||||
if request.request_id in self._active_streams:
|
||||
index -= 1
|
||||
continue
|
||||
request = running_queue.pop(index)
|
||||
self._held_non_active.append(request)
|
||||
index -= 1
|
||||
|
||||
def _process_chunk_queue_legacy(
|
||||
self,
|
||||
queue: Any,
|
||||
waiting_for_chunk_list: deque[Any],
|
||||
target_status: RequestStatus,
|
||||
finished_load_reqs: set[str],
|
||||
) -> None:
|
||||
queue_snapshot = list(queue)
|
||||
for request in queue_snapshot:
|
||||
if request.status != RequestStatus.WAITING_FOR_CHUNK:
|
||||
if request.request_id in self.requests_with_ready_chunks:
|
||||
# Requests that have loaded chunk from last round
|
||||
# of schedule, but have not scheduled
|
||||
continue
|
||||
if request.request_id in self.finished_requests:
|
||||
continue
|
||||
# Requests that waiting for chunk
|
||||
self.load_async(request)
|
||||
request.status = RequestStatus.WAITING_FOR_CHUNK
|
||||
else:
|
||||
if request.request_id in finished_load_reqs:
|
||||
request.status = target_status
|
||||
finished_load_reqs.remove(request.request_id)
|
||||
self.requests_with_ready_chunks.add(request.request_id)
|
||||
continue
|
||||
queue.remove(request)
|
||||
self.requests_origin_status[request.request_id] = target_status
|
||||
waiting_for_chunk_list.append(request)
|
||||
|
||||
def restore_queues(
|
||||
self,
|
||||
@@ -459,6 +581,10 @@ class OmniChunkTransferAdapter(OmniTransferAdapterBase):
|
||||
running_queue.extend(live_running_requests)
|
||||
self.waiting_for_chunk_running_requests = deque()
|
||||
|
||||
if self._held_non_active:
|
||||
running_queue.extend(self._held_non_active)
|
||||
self._held_non_active = deque()
|
||||
|
||||
def postprocess_scheduler_output(
|
||||
self,
|
||||
scheduler_output: Any,
|
||||
@@ -475,7 +601,25 @@ class OmniChunkTransferAdapter(OmniTransferAdapterBase):
|
||||
|
||||
if requests is not None:
|
||||
self.attach_cached_additional_information(scheduler_output, requests)
|
||||
scheduled_req_ids = self._scheduled_request_ids(scheduler_output)
|
||||
self._clear_chunk_ready(scheduler_output)
|
||||
if scheduled_req_ids:
|
||||
# Terminal chunks must stay active until they are scheduled once.
|
||||
self._evict_finished_active_streams(scheduled_req_ids)
|
||||
|
||||
@staticmethod
|
||||
def _scheduled_request_ids(scheduler_output: Any) -> set[str]:
|
||||
req_ids: set[str] = set()
|
||||
if scheduler_output.scheduled_new_reqs:
|
||||
for req_data in scheduler_output.scheduled_new_reqs:
|
||||
req_id = getattr(req_data, "req_id", None)
|
||||
if req_id:
|
||||
req_ids.add(req_id)
|
||||
if scheduler_output.scheduled_cached_reqs:
|
||||
for req_id in scheduler_output.scheduled_cached_reqs.req_ids:
|
||||
if req_id:
|
||||
req_ids.add(req_id)
|
||||
return req_ids
|
||||
|
||||
@staticmethod
|
||||
def attach_cached_additional_information(scheduler_output: Any, requests: dict[str, Request]) -> None:
|
||||
@@ -500,6 +644,8 @@ class OmniChunkTransferAdapter(OmniTransferAdapterBase):
|
||||
) -> None:
|
||||
queue_snapshot = list(queue)
|
||||
for request in queue_snapshot:
|
||||
if not self._ensure_active_stream(request):
|
||||
continue
|
||||
if request.status != RequestStatus.WAITING_FOR_CHUNK:
|
||||
if request.request_id in self.requests_with_ready_chunks:
|
||||
# Requests that have loaded chunk from last round
|
||||
|
||||
@@ -139,6 +139,10 @@ class OmniEngineArgs(EngineArgs):
|
||||
stage_connector_spec: dict[str, Any] = field(default_factory=dict)
|
||||
subtalker_sampling_params: dict[str, Any] | None = None
|
||||
async_chunk: bool = False
|
||||
# WS-A: Stage-1 active stream slots. 0 = legacy preempt-everything.
|
||||
# Must be declared here so engine_args dict propagation does not silently
|
||||
# drop the value when constructing OmniEngineArgs from kwargs.
|
||||
active_stream_window: int = 0
|
||||
omni_kv_config: dict | None = None
|
||||
quantization_config: Any | None = None
|
||||
force_cutlass_fp8: bool | None = None
|
||||
@@ -339,6 +343,7 @@ class OmniEngineArgs(EngineArgs):
|
||||
# All kwargs below are Omni specific
|
||||
stage_id=self.stage_id,
|
||||
async_chunk=self.async_chunk,
|
||||
active_stream_window=self.active_stream_window,
|
||||
model_stage=self.model_stage,
|
||||
model_arch=self.model_arch,
|
||||
worker_type=self.worker_type,
|
||||
|
||||
Reference in new issue
Block a user