[Feature]Support async computation and communication across stages by chunks (#727)

Signed-off-by: amy-why-3459 <wuhaiyan17@huawei.com>
Signed-off-by: Rein Yang <ruiruyang2@gmail.com>
Co-authored-by: Rein Yang <ruiruyang2@gmail.com>
Co-authored-by: CHEN <116010019@link.cuhk.edu.cn>
Co-authored-by: Hongsheng Liu <liuhongsheng4@huawei.com>
This commit is contained in:
authored and GitHub committed 2026-01-26 11:21:24 +08:00
1 parent 578ed1967e
commit 52bf1e2f40
29 files changed
+1258 -396

No files matched your search

+2
View File
@@ -53,8 +53,10 @@ Engine classes for offline and online inference.
Core scheduling and caching components.
- [vllm_omni.core.sched.omni_ar_scheduler.KVCacheTransferData][]
- [vllm_omni.core.sched.omni_ar_scheduler.OmniARScheduler][]
- [vllm_omni.core.sched.omni_generation_scheduler.OmniGenerationScheduler][]
- [vllm_omni.core.sched.output.OmniCachedRequestData][]
- [vllm_omni.core.sched.output.OmniNewRequestData][]
- [vllm_omni.model_executor.models.qwen3_tts.tokenizer_25hz.vq.core_vq.DistributedGroupResidualVectorQuantization][]
- [vllm_omni.model_executor.models.qwen3_tts.tokenizer_25hz.vq.core_vq.DistributedResidualVectorQuantization][]
@@ -15,6 +15,12 @@ Please refer to [README.md](https://github.com/vllm-project/vllm-omni/tree/main/
vllm serve Qwen/Qwen3-Omni-30B-A3B-Instruct --omni --port 8091
```
If you want to open async chunking for qwen3-omni, launch the server with command below
```bash
vllm serve Qwen/Qwen3-Omni-30B-A3B-Instruct --omni --port 8091 --stage-configs-path /vllm_omni/model_executor/stage_configs/qwen3_omni_moe_async_chunk.yaml
```
If you have custom stage configs file, launch the server with command below
```bash
vllm serve Qwen/Qwen3-Omni-30B-A3B-Instruct --omni --port 8091 --stage-configs-path /path/to/stage_configs_file
@@ -12,6 +12,12 @@ Please refer to [README.md](../../../README.md)
vllm serve Qwen/Qwen3-Omni-30B-A3B-Instruct --omni --port 8091
```
If you want to open async chunking for qwen3-omni, launch the server with command below
```bash
vllm serve Qwen/Qwen3-Omni-30B-A3B-Instruct --omni --port 8091 --stage-configs-path /vllm_omni/model_executor/stage_configs/qwen3_omni_moe_async_chunk.yaml
```
If you have custom stage configs file, launch the server with command below
```bash
vllm serve Qwen/Qwen3-Omni-30B-A3B-Instruct --omni --port 8091 --stage-configs-path /path/to/stage_configs_file
@@ -1,4 +1,5 @@
import base64
import concurrent.futures
import os
from typing import NamedTuple
@@ -414,51 +415,67 @@ def run_multimodal_generation(args) -> None:
else:
output_modalities = None
chat_completion = client.chat.completions.create(
messages=[
get_system_prompt(),
prompt,
],
model=model_name,
modalities=output_modalities,
extra_body=extra_body,
stream=args.stream,
)
# Test multiple concurrent completions
num_concurrent_requests = args.num_concurrent_requests
with concurrent.futures.ThreadPoolExecutor(max_workers=num_concurrent_requests) as executor:
# Submit multiple completion requests concurrently
futures = [
executor.submit(
client.chat.completions.create,
messages=[
get_system_prompt(),
prompt,
],
model=model_name,
modalities=output_modalities,
extra_body=extra_body,
stream=args.stream,
)
for _ in range(num_concurrent_requests)
]
# Wait for all requests to complete and collect results
chat_completions = [future.result() for future in concurrent.futures.as_completed(futures)]
assert len(chat_completions) == num_concurrent_requests
count = 0
if not args.stream:
for choice in chat_completion.choices:
if choice.message.audio:
audio_data = base64.b64decode(choice.message.audio.data)
audio_file_path = f"audio_{count}.wav"
with open(audio_file_path, "wb") as f:
f.write(audio_data)
print(f"Audio saved to {audio_file_path}")
count += 1
elif choice.message.content:
print("Chat completion output from text:", choice.message.content)
else:
printed_content = False
for chunk in chat_completion:
for choice in chunk.choices:
if hasattr(choice, "delta"):
content = getattr(choice.delta, "content", None)
else:
content = None
if getattr(chunk, "modality", None) == "audio" and content:
audio_data = base64.b64decode(content)
# Verify all completions succeeded
for chat_completion in chat_completions:
for choice in chat_completion.choices:
if choice.message.audio:
audio_data = base64.b64decode(choice.message.audio.data)
audio_file_path = f"audio_{count}.wav"
with open(audio_file_path, "wb") as f:
f.write(audio_data)
print(f"\nAudio saved to {audio_file_path}")
print(f"Audio saved to {audio_file_path}")
count += 1
elif choice.message.content:
print("Chat completion output from text:", choice.message.content)
else:
printed_content = False
for chat_completion in chat_completions:
for chunk in chat_completion:
for choice in chunk.choices:
if hasattr(choice, "delta"):
content = getattr(choice.delta, "content", None)
else:
content = None
elif getattr(chunk, "modality", None) == "text":
if not printed_content:
printed_content = True
print("\ncontent:", end="", flush=True)
print(content, end="", flush=True)
if getattr(chunk, "modality", None) == "audio" and content:
audio_data = base64.b64decode(content)
audio_file_path = f"audio_{count}.wav"
with open(audio_file_path, "wb") as f:
f.write(audio_data)
print(f"\nAudio saved to {audio_file_path}")
count += 1
elif getattr(chunk, "modality", None) == "text":
if not printed_content:
printed_content = True
print("\ncontent:", end="", flush=True)
print(content, end="", flush=True)
def parse_args():
@@ -517,6 +534,12 @@ def parse_args():
action="store_true",
help="Stream the response.",
)
parser.add_argument(
"--num-concurrent-requests",
type=int,
default=1,
help="Number of concurrent requests to send. Default is 1.",
)
return parser.parse_args()
+12
View File
@@ -1,4 +1,5 @@
import warnings
from dataclasses import field
from typing import Any
import torch
@@ -42,6 +43,7 @@ class OmniModelConfig(ModelConfig):
Attributes:
stage_id: Identifier for the stage in a multi-stage pipeline (default: 0)
async_chunk: If set to True, perform async chunk
model_stage: Stage type identifier, e.g., "thinker" or "talker"
(default: "thinker")
model_arch: Model architecture name
@@ -49,6 +51,8 @@ class OmniModelConfig(ModelConfig):
engine_output_type: Optional output type specification for the engine.
Used to route outputs to appropriate processors (e.g., "image",
"audio", "latents"). If None, output type is inferred.
stage_connector_config: Stage connector configuration dictionary.
Contains "name" (connector name), "extra" (extra connector config).
Example:
>>> config = OmniModelConfig(
@@ -59,10 +63,18 @@ class OmniModelConfig(ModelConfig):
"""
stage_id: int = 0
async_chunk: bool = False
model_stage: str = "thinker"
model_arch: str = "Qwen2_5OmniForConditionalGeneration"
engine_output_type: str | None = None
hf_config_name: str | None = None
custom_process_next_stage_input_func: str | None = None
stage_connector_config: dict[str, Any] = field(
default_factory=lambda: {
"name": "SharedMemoryConnector",
"extra": {},
}
)
omni_kv_config: dict | None = None
@property
+29
View File
@@ -1,5 +1,6 @@
from __future__ import annotations
import importlib
from collections import defaultdict
from dataclasses import asdict, dataclass
from time import time
@@ -19,6 +20,10 @@ from vllm.v1.outputs import ModelRunnerOutput
from vllm.v1.request import Request, RequestStatus
from vllm.v1.spec_decode.metrics import SpecDecodingStats
from vllm_omni.distributed.omni_connectors.adapter import get_chunk, put_chunk
from vllm_omni.distributed.omni_connectors.factory import OmniConnectorFactory
from vllm_omni.distributed.omni_connectors.utils.config import ConnectorSpec
logger = init_logger(__name__)
@@ -59,6 +64,25 @@ class OmniARScheduler(VLLMScheduler):
# Track requests that have already triggered prefill transfer to avoid duplicates
self.transfer_triggered_requests: set[str] = set()
model_config = self.vllm_config.model_config
self.omni_connector = None
if model_config.async_chunk:
connector_config = model_config.stage_connector_config
connector_specs = ConnectorSpec(
name=connector_config.get("name", "SharedMemoryConnector"),
extra=connector_config.get("extra", {}),
)
self.omni_connector = OmniConnectorFactory.create_connector(connector_specs)
custom_process_next_stage_input_func = getattr(
self.vllm_config.model_config, "custom_process_next_stage_input_func", None
)
if custom_process_next_stage_input_func:
module_path, func_name = custom_process_next_stage_input_func.rsplit(".", 1)
module = importlib.import_module(module_path)
self.custom_process_next_stage_input_func = getattr(module, func_name)
self.stage_id = getattr(self.vllm_config.model_config, "stage_id", None)
def _get_kv_transfer_criteria(self) -> dict | None:
# Note: vllm_config is available in Scheduler after super().__init__
@@ -156,6 +180,8 @@ class OmniARScheduler(VLLMScheduler):
new_list.append(omni_nr)
scheduler_output.scheduled_new_reqs = new_list # type: ignore[assignment]
if self.omni_connector is not None:
get_chunk(self.omni_connector, scheduler_output)
# Add information about requests needing KV cache transfer
scheduler_output.finished_requests_needing_kv_transfer = self.get_finished_requests_needing_kv_transfer()
@@ -337,6 +363,9 @@ class OmniARScheduler(VLLMScheduler):
num_nans_in_logits=request.num_nans_in_logits,
)
)
if self.omni_connector is not None:
custom_process_next_stage_input_func = self.custom_process_next_stage_input_func
put_chunk(self.omni_connector, pooler_output, request, custom_process_next_stage_input_func)
else:
# Invariant: EngineCore returns no partial prefill outputs.
assert not prompt_logprobs_tensors
@@ -12,11 +12,27 @@ from vllm.v1.engine import EngineCoreEventType, EngineCoreOutput, EngineCoreOutp
from vllm.v1.request import Request, RequestStatus
from vllm.v1.spec_decode.metrics import SpecDecodingStats
from vllm_omni.core.sched.output import OmniNewRequestData
from vllm_omni.core.sched.output import OmniCachedRequestData, OmniNewRequestData
from vllm_omni.distributed.omni_connectors.adapter import get_chunk_for_generation
from vllm_omni.distributed.omni_connectors.factory import OmniConnectorFactory
from vllm_omni.distributed.omni_connectors.utils.config import ConnectorSpec
from vllm_omni.outputs import OmniModelRunnerOutput
class OmniGenerationScheduler(VLLMScheduler):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
model_config = self.vllm_config.model_config
self.omni_connector = None
if model_config.async_chunk:
connector_config = model_config.stage_connector_config
connector_specs = ConnectorSpec(
name=connector_config.get("name", "SharedMemoryConnector"),
extra=connector_config.get("extra", {}),
)
self.omni_connector = OmniConnectorFactory.create_connector(connector_specs)
self.stage_id = getattr(self.vllm_config.model_config, "stage_id", None)
def schedule(self) -> SchedulerOutput:
"""Diffusion fast path:
- Feed all input tokens of the request at once
@@ -32,27 +48,54 @@ class OmniGenerationScheduler(VLLMScheduler):
req_to_new_blocks: dict[str, KVCacheBlocks] = {}
num_scheduled_tokens: dict[str, int] = {}
scheduled_running_reqs: list[Request] = []
scheduled_spec_decode_tokens: dict[str, list[int]] = {}
scheduled_encoder_inputs: dict[str, list[int]] = {}
cached_prompt_token_ids: dict[str, list[int]] = {}
# Temporary queue: preserve waiting order, do not disturb non-diffusion requests
skipped_waiting_requests = create_request_queue(self.policy)
req_index = 0
while req_index < len(self.running) and token_budget > 0:
request = self.running[req_index]
if self.omni_connector is not None:
get_chunk_for_generation(self.omni_connector, request)
num_computed_tokens = request.num_computed_tokens
required_tokens = max(len(request.prompt_token_ids) - num_computed_tokens, 1)
num_new_tokens = min(required_tokens, token_budget)
new_blocks = self.kv_cache_manager.allocate_slots(
request,
num_new_tokens,
num_lookahead_tokens=self.num_lookahead_tokens,
)
if new_blocks is None:
# Allocation failed (e.g., VRAM pressure); stop fast path and
# fall back to default scheduling
# Put the current request back to the head of the waiting queue
# Note: the original queue order is preserved
break
if self.log_stats:
request.record_event(EngineCoreEventType.SCHEDULED, scheduled_timestamp)
req_to_new_blocks[request.request_id] = new_blocks
num_scheduled_tokens[request.request_id] = num_new_tokens
cached_prompt_token_ids[request.request_id] = request.prompt_token_ids
token_budget -= num_new_tokens
scheduled_running_reqs.append(request)
req_index += 1
# Fast path selection and scheduling (treat all as diffusion requests,
# independent of pooling_params)
while self.waiting and token_budget > 0 and len(self.running) < self.max_num_running_reqs:
request = self.waiting.peek_request()
if self.omni_connector is not None:
get_chunk_for_generation(self.omni_connector, request)
# Uniformly treat as diffusion. A feature flag can be added later
# via config or request tag.
# Allocate all input tokens for the request in one shot
# (allocate 1 placeholder if zero)
required_tokens = max(getattr(request, "num_prompt_tokens", 0), 1)
if required_tokens > token_budget:
# Insufficient budget to process all inputs at once;
# stop fast path attempt
break
num_new_tokens = required_tokens
required_tokens = max(len(request.prompt_token_ids), 1)
num_new_tokens = min(required_tokens, token_budget)
new_blocks = self.kv_cache_manager.allocate_slots(
request,
num_new_tokens,
@@ -68,7 +111,6 @@ class OmniGenerationScheduler(VLLMScheduler):
# Officially schedule this request
request = self.waiting.pop_request()
self.running.append(request)
request.status = RequestStatus.RUNNING
if self.log_stats:
request.record_event(EngineCoreEventType.SCHEDULED, scheduled_timestamp)
@@ -109,13 +151,24 @@ class OmniGenerationScheduler(VLLMScheduler):
]
# No running/resumed reqs scheduled in our fast path
cached_reqs_data = self._make_cached_request_data(
running_reqs=[],
running_reqs=scheduled_running_reqs,
resumed_reqs=[],
num_scheduled_tokens=num_scheduled_tokens,
spec_decode_tokens=scheduled_spec_decode_tokens,
req_to_new_blocks=req_to_new_blocks,
)
cached_reqs_data = OmniCachedRequestData(
req_ids=cached_reqs_data.req_ids,
resumed_req_ids=cached_reqs_data.resumed_req_ids,
new_token_ids=cached_reqs_data.new_token_ids,
all_token_ids=cached_reqs_data.all_token_ids,
new_block_ids=cached_reqs_data.new_block_ids,
num_computed_tokens=cached_reqs_data.num_computed_tokens,
num_output_tokens=cached_reqs_data.num_output_tokens,
prompt_token_ids=cached_prompt_token_ids,
)
total_num_scheduled_tokens = sum(num_scheduled_tokens.values())
scheduler_output = SchedulerOutput(
scheduled_new_reqs=new_reqs_data,
@@ -257,21 +310,20 @@ class OmniGenerationScheduler(VLLMScheduler):
new_logprobs = None
new_token_ids = generated_token_ids
kv_transfer_params = None
status_before_stop = request.status
pooler_output = None
if pooler_outputs:
pooler_output = pooler_outputs[req_index]
# Diffusion request: completes in one step; mark finished and free resources
request.status = RequestStatus.FINISHED_STOPPED
# Optional: set a stop_reason for front-end clarity
# (does not affect protocol)
request.stop_reason = request.stop_reason # or "generation_done"
kv_transfer_params = self._free_request(request)
if status_before_stop == RequestStatus.RUNNING:
if request.status == RequestStatus.FINISHED_STOPPED or (
self.omni_connector is None and request.num_computed_tokens >= request.num_prompt_tokens
):
request.status = RequestStatus.FINISHED_STOPPED
# Optional: set a stop_reason for front-end clarity
# (does not affect protocol)
request.stop_reason = request.stop_reason # or "generation_done"
kv_transfer_params = self._free_request(request)
stopped_running_reqs.add(request)
else:
stopped_preempted_reqs.add(request)
# Extract sample logprobs if needed.
if request.sampling_params is not None and request.sampling_params.logprobs is not None and logprobs:
+12 -1
View File
@@ -1,6 +1,6 @@
from dataclasses import dataclass
from vllm.v1.core.sched.output import NewRequestData
from vllm.v1.core.sched.output import CachedRequestData, NewRequestData
from vllm.v1.request import Request
from vllm_omni.engine import AdditionalInformationPayload, PromptEmbedsPayload
@@ -53,3 +53,14 @@ class OmniNewRequestData(NewRequestData):
prefill_token_ids=prefill_token_ids,
additional_information=request.additional_information,
)
@dataclass
class OmniCachedRequestData(CachedRequestData):
"""Cached request data for omni models with embeddings support.
Args:
prompt_token_ids: Mapping from request ID to list of prompt token IDs
"""
prompt_token_ids: dict[int, list[int]]
@@ -207,10 +207,11 @@ class GPUDiffusionModelRunner:
start_time = time.time()
while True:
get_key = f"omni_{from_stage}_to_{to_stage}_kv_cache_{req.request_id}"
result = self.connector.get(
from_stage=from_stage,
to_stage=to_stage,
request_id=f"kv_cache_{req.request_id}",
get_key=get_key,
)
if result:
break
@@ -5,7 +5,14 @@
import time
from collections.abc import Callable
from typing import Any
from typing import TYPE_CHECKING, Any
import torch
from vllm.v1.core.sched.output import SchedulerOutput
from vllm.v1.request import Request, RequestStatus
if TYPE_CHECKING:
from .connectors.base import OmniConnectorBase
from vllm_omni.entrypoints.stage_utils import OmniStageTaskType
@@ -170,3 +177,225 @@ def try_recv_via_connector(
# but for Stage-0 seed it should be there.
# We'll return None to let caller handle error if strictly required.
return None, None
def get_chunk(
connector: "OmniConnectorBase",
scheduler_output: SchedulerOutput,
) -> None:
"""Retrieve a chunk of pooling output.
Args:
connector: OmniConnectorBase instance
scheduler_output: Partial scheduler output dictionary
Returns:
None: This function modifies scheduler_output in place
"""
stage_id = connector.stage_id
if stage_id == 0:
return
target_stage_id = stage_id - 1
# Handle new requests
for new_req_data in scheduler_output.scheduled_new_reqs:
req_id = new_req_data.req_id[0:25]
chunk_id = connector.get_requests[req_id]
connector_get_key = f"{req_id}_{target_stage_id}_{chunk_id}"
payload_data = get_through_connector(connector, target_stage_id, stage_id, req_id, connector_get_key)
if payload_data:
new_req_data.additional_information = payload_data
if payload_data.get("finished"):
connector.finished_requests.add(req_id)
# Handle cached/running requests
cached_reqs = scheduler_output.scheduled_cached_reqs
if not hasattr(cached_reqs, "additional_information"):
cached_reqs.additional_information = {}
for i, cached_req_id in enumerate(cached_reqs.req_ids):
req_id = cached_req_id[0:25]
if req_id in connector.finished_requests:
continue
chunk_id = connector.get_requests[req_id]
connector_get_key = f"{req_id}_{target_stage_id}_{chunk_id}"
payload_data = get_through_connector(connector, target_stage_id, stage_id, req_id, connector_get_key)
if payload_data:
cached_reqs.additional_information[cached_req_id] = payload_data
if payload_data.get("finished"):
connector.finished_requests.add(req_id)
def get_through_connector(connector, target_stage_id, stage_id, req_id, connector_get_key):
# Wait for data from previous stage
import time
# TODO: add correct check mechanism for the payload_data
max_wait = 300
for _ in range(max_wait):
result = connector.get(
from_stage=str(target_stage_id),
to_stage=str(stage_id),
get_key=connector_get_key,
)
payload_data = None
if result:
payload_data, size = result
if payload_data:
connector.request_prompt_token_ids[req_id] = payload_data.get("thinker_input_ids", [])
connector.get_requests[req_id] += 1
logger.debug(f"[Stage-{stage_id}] Received one chunk for request {connector_get_key}")
break
time.sleep(0.1)
return payload_data
def get_chunk_for_generation(
connector: "OmniConnectorBase",
request: Request,
) -> None:
"""Retrieve a chunk of pooling output.
Args:
connector: OmniConnectorBase instance
request: Request object
Returns:
None: This function modifies request in place
"""
stage_id = connector.stage_id
target_stage_id = stage_id - 1
request_id = request.request_id[0:25]
if request_id in connector.finished_requests:
return
chunk_id = connector.get_requests[request_id]
connector_get_key = f"{request_id}_{target_stage_id}_{chunk_id}"
payload_data = get_through_connector(connector, target_stage_id, stage_id, request_id, connector_get_key)
if not payload_data:
return
if payload_data.get("finished"):
connector.finished_requests.add(request_id)
request.status = RequestStatus.FINISHED_STOPPED
# TODO: remove special handling for prompt token ids ?
if chunk_id == 0:
request.prompt_token_ids = payload_data.get("code_predictor_codes", [])
else:
request.prompt_token_ids += payload_data.get("code_predictor_codes", [])
def put_chunk(
connector: "OmniConnectorBase",
pooling_output: dict[str, Any],
request: Request,
custom_process_input_func: Callable[[dict[str, Any], Request], dict[str, Any] | None] | None = None,
) -> None:
"""Store a chunk of pooling output.
Args:
connector: OmniConnectorBase instance
pooling_output: Partial pooling output dictionary
request: Request object
custom_process_input_func: Optional custom function to process input
Returns:
None: This function sends data via connector
"""
stage_id = connector.stage_id
next_stage_id = stage_id + 1
request_id = request.request_id[0:25]
prompt_token_ids = request.prompt_token_ids
connector.request_prompt_token_ids[request_id] = prompt_token_ids
chunk_id = connector.put_requests[request_id]
connector_put_key = f"{request_id}_{stage_id}_{chunk_id}"
payload_data = None
# TODO: add default process_input_func to handle the payload_data ?
if custom_process_input_func:
try:
payload_data = custom_process_input_func(
pooling_output=pooling_output,
request=request,
)
except Exception as e:
logger.error(f"Failed to use custom_process_input_func for payload extraction: {e}")
if not payload_data:
logger.warning(f"[Stage-{stage_id}] No payload data to send for request {request_id}")
return
if stage_id == 0 and chunk_id == 0:
if connector.request_payload.get(request_id) is None:
connector.request_payload[request_id] = payload_data
return
else:
save_payload = connector.request_payload.get(request_id)
payload_data["thinker_embeddings"] = torch.cat(
(save_payload.get("thinker_embeddings"), payload_data.get("thinker_embeddings")), dim=0
)
payload_data["thinker_hidden_states"] = torch.cat(
(save_payload.get("thinker_hidden_states"), payload_data.get("thinker_hidden_states")), dim=0
)
logger.info(f"[Stage-{stage_id}] Merged embeddings and hidden states for request {request_id}")
if stage_id == 1:
# TODO: Make parameters configurable and optimize algorithms
chunk_size = left_context_size = 25
connector.code_prompt_token_ids[request_id].append(payload_data.get("code_predictor_codes", []))
length = len(connector.code_prompt_token_ids[request_id])
chunk_length = length % chunk_size
if chunk_length != 0 and not payload_data.get("finished"):
return
context_length = chunk_length if chunk_length != 0 else chunk_size
end_index = min(length, left_context_size + context_length)
payload_data["code_predictor_codes"] = (
torch.tensor(connector.code_prompt_token_ids[request_id][-end_index:])
.transpose(0, 1)
.reshape(-1)
.tolist()
)
success, size, metadata = connector.put(
from_stage=str(stage_id), to_stage=str(next_stage_id), put_key=connector_put_key, data=payload_data
)
if success:
connector.put_requests[request_id] += 1
logger.info(f"[Stage-{stage_id}] Sent {connector_put_key}")
def compute_talker_prompt_ids_length(prompt_ids: list[int]) -> int:
"""Compute the length of the talker prompt ids.
Args:
prompt_ids: The prompt ids tensor.
Returns:
The length of the talker prompt ids.
"""
im_start_token_id = 151644
system_token_id = 8948
user_token_id = 872
assistant_token_id = 77091
im_start_indexes = [i for i in range(len(prompt_ids)) if prompt_ids[i] == im_start_token_id]
im_start_indexes.append(len(prompt_ids))
sum_user_len = 0
assistant_len = 0
for i in range(len(im_start_indexes) - 1):
s = im_start_indexes[i]
e = im_start_indexes[i + 1]
role = prompt_ids[s + 1]
if role == system_token_id:
continue
elif role == user_token_id:
sum_user_len += e - s
elif role == assistant_token_id and i == len(im_start_indexes) - 2:
assistant_len += 9 # 3 + 4 + 1 + 1
else:
pass
return sum_user_len + assistant_len
@@ -13,15 +13,13 @@ class OmniConnectorBase(ABC):
"""Base class for all OmniConnectors."""
@abstractmethod
def put(
self, from_stage: str, to_stage: str, request_id: str, data: Any
) -> tuple[bool, int, dict[str, Any] | None]:
def put(self, from_stage: str, to_stage: str, put_key: str, data: Any) -> tuple[bool, int, dict[str, Any] | None]:
"""Store Python object, internal serialization handled by connector.
Args:
from_stage: Source stage identifier
to_stage: Destination stage identifier
request_id: Unique request identifier
put_key: Unique request identifier
data: Python object to store
Returns:
@@ -31,16 +29,13 @@ class OmniConnectorBase(ABC):
pass
@abstractmethod
def get(
self, from_stage: str, to_stage: str, request_id: str, metadata: dict[str, Any] | None = None
) -> tuple[Any, int] | None:
def get(self, from_stage: str, to_stage: str, get_key: str, metadata=None) -> tuple[Any, int] | None:
"""Retrieve Python object and payload size (bytes).
Args:
from_stage: Source stage identifier
to_stage: Destination stage identifier
request_id: Unique request identifier
metadata: Optional transport-specific metadata from the put operation
get_key: Unique request identifier
Returns:
Tuple of (Python object, serialized byte size) if found, None otherwise
@@ -1,6 +1,8 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
import time
from collections import defaultdict
from typing import Any
from vllm_omni.entrypoints.stage_utils import shm_read_bytes, shm_write_bytes
@@ -19,6 +21,14 @@ class SharedMemoryConnector(OmniConnectorBase):
def __init__(self, config: dict[str, Any]):
self.config = config
self.stage_id = config.get("stage_id", -1)
self.device = config.get("device", "cuda:0")
self.put_requests: dict[str, int] = defaultdict(int)
self.get_requests: dict[str, int] = defaultdict(int)
self.finished_requests: set[str] = set()
self.request_payload = {}
self.request_prompt_token_ids: dict[str, list[int]] = defaultdict(list)
self.code_prompt_token_ids: dict[str, list[list[int]]] = defaultdict(list)
# Default threshold matches legacy behavior (64KB)
self.threshold = int(config.get("shm_threshold_bytes", 65536))
self._metrics = {
@@ -29,9 +39,7 @@ class SharedMemoryConnector(OmniConnectorBase):
"inline_writes": 0,
}
def put(
self, from_stage: str, to_stage: str, request_id: str, data: Any
) -> tuple[bool, int, dict[str, Any] | None]:
def put(self, from_stage: str, to_stage: str, put_key: str, data: Any) -> tuple[bool, int, dict[str, Any] | None]:
try:
# Always serialize first to check size (and for SHM writing)
# Note: For extremely large objects in "inline" mode (e.g. Ray),
@@ -40,18 +48,19 @@ class SharedMemoryConnector(OmniConnectorBase):
payload = self.serialize_obj(data)
size = len(payload)
if size > self.threshold:
metadata = {}
# if size > self.threshold:
if True: # TODO: correct put & get logic
# Use Shared Memory
shm_name = f"omni_{from_stage}_to_{to_stage}_{request_id}"
meta = shm_write_bytes(payload, name=shm_name)
meta = shm_write_bytes(payload, name=put_key)
# meta contains {'name': ..., 'size': ...}
metadata = {"shm": meta, "size": size}
metadata[put_key] = {"shm": meta, "size": size}
self._metrics["shm_writes"] += 1
else:
# Inline - pass bytes directly to avoid double serialization of the object
# We already serialized it to check size, so we pass the bytes.
# The Queue will pickle these bytes (fast), avoiding re-serializing the complex object.
metadata = {"inline_bytes": payload, "size": size}
metadata[put_key] = {"inline_bytes": payload, "size": size}
self._metrics["inline_writes"] += 1
self._metrics["puts"] += 1
@@ -60,83 +69,40 @@ class SharedMemoryConnector(OmniConnectorBase):
return True, size, metadata
except Exception as e:
logger.error(f"SharedMemoryConnector put failed for req {request_id}: {e}")
logger.error(f"SharedMemoryConnector put failed for req {put_key}: {e}")
return False, 0, None
def get(
self, from_stage: str, to_stage: str, request_id: str, metadata: dict[str, Any] | None = None
) -> tuple[Any, int] | None:
# Helper to try reading SHM with a specific name
def try_read_shm(name: str):
# _shm is imported at module level as: import multiprocessing.shared_memory as _shm
# but to be safe inside this closure if not picked up:
from multiprocessing import shared_memory as shm_pkg
def get(self, from_stage: str, to_stage: str, get_key: str, metadata=None) -> tuple[Any, int] | None:
from multiprocessing import shared_memory as shm_pkg
shm = shm_pkg.SharedMemory(name=name)
# Wait for shared memory to be available (with retry logic)
max_retries = 30
retry_delay = 0.1 # 100ms between retries
shm = None
for attempt in range(max_retries):
try:
# We assume the buffer size matches the data size
return shm_read_bytes({"name": name, "size": shm.size})
finally:
shm.close()
shm = shm_pkg.SharedMemory(name=get_key)
break # Successfully opened, exit retry loop
except FileNotFoundError:
if attempt < max_retries - 1:
time.sleep(retry_delay)
else:
# Max retries reached, return None
logger.warning(f"Shared memory '{get_key}' not found after {max_retries} retries")
return None, 0
if not metadata:
# Try to infer metadata from request_id for KV cache transfer scenarios
# where metadata is not passed out-of-band.
shm_name = f"omni_{from_stage}_to_{to_stage}_{request_id}"
# We need the size to read from SHM using stage_utils.shm_read_bytes
try:
# Use local import to avoid potential scope issues if _shm isn't available
from multiprocessing import shared_memory as shm_pkg
temp_shm = shm_pkg.SharedMemory(name=shm_name)
size = temp_shm.size
temp_shm.close()
metadata = {"shm": {"name": shm_name, "size": size}, "is_fallback": True}
logger.debug(f"SharedMemoryConnector inferred metadata for {request_id}: {metadata}")
except Exception as e:
# If file not found or other error, standard path will likely fail too, but we let it try or return None
logger.debug(f"SharedMemoryConnector failed to infer metadata for {request_id} (fallback): {e}")
# We can't proceed without size if we rely on shm_read_bytes
return None
if shm is None:
return None, 0
try:
obj = None
size = 0
data_bytes = shm_read_bytes({"name": get_key, "size": shm.size})
obj = self.deserialize_obj(data_bytes)
return obj, shm.size
finally:
shm.close()
if "shm" in metadata:
meta = metadata["shm"]
# shm_read_bytes handles reading and unlinking
data_bytes = shm_read_bytes(meta)
obj = self.deserialize_obj(data_bytes)
size = metadata.get("size", len(data_bytes))
elif "inline_bytes" in metadata:
# Deserialize bytes back to object
payload = metadata["inline_bytes"]
obj = self.deserialize_obj(payload)
size = metadata.get("size", len(payload))
elif "inline" in metadata:
obj = metadata["inline"]
size = metadata.get("size", 0)
if size == 0:
# Fallback if size wasn't recorded
try:
size = len(self.serialize_obj(obj))
except Exception:
pass
else:
logger.error(
f"Unknown metadata format in SharedMemoryConnector for req {request_id}: {list(metadata.keys())}"
)
return None
self._metrics["gets"] += 1
return obj, size
except Exception as e:
logger.error(f"SharedMemoryConnector get failed for req {request_id}: {e}")
return None
# TODO: update another read method
def cleanup(self, request_id: str) -> None:
# SHM segments are automatically unlinked during 'get' (shm_read_bytes).
@@ -91,6 +91,8 @@ def get_connectors_config_for_stage(transfer_config: OmniTransferConfig | None,
# (Worker needs to create connectors to receive data)
if to_stage == target_stage:
stage_connectors_config[f"from_stage_{from_stage}"] = {"spec": {"name": spec.name, "extra": spec.extra}}
elif from_stage == target_stage and target_stage == "0":
stage_connectors_config[f"to_stage_{to_stage}"] = {"spec": {"name": spec.name, "extra": spec.extra}}
return stage_connectors_config
+31 -1
View File
@@ -1,4 +1,4 @@
from dataclasses import dataclass
from dataclasses import dataclass, field
from transformers.models.qwen3_omni_moe.configuration_qwen3_omni_moe import Qwen3OmniMoeTextConfig
from vllm.engine.arg_utils import EngineArgs
@@ -56,6 +56,11 @@ class OmniEngineArgs(EngineArgs):
engine_output_type: Optional output type specification for the engine.
Used to route outputs to appropriate processors (e.g., "image",
"audio", "latents"). If None, output type is inferred.
custom_process_next_stage_input_func: Optional path to a custom function for processing
inputs from previous stages
If None, default processing is used.
stage_connector_spec: Extra configuration for stage connector
async_chunk: If set to True, perform async chunk
"""
stage_id: int = 0
@@ -63,6 +68,9 @@ class OmniEngineArgs(EngineArgs):
model_arch: str = "Qwen2_5OmniForConditionalGeneration"
engine_output_type: str | None = None
hf_config_name: str | None = None
custom_process_next_stage_input_func: str | None = None
stage_connector_spec: dict[str, any] = field(default_factory=dict)
async_chunk: bool = False
omni_kv_config: dict | None = None
def draw_hf_text_config(self, config_dict: dict) -> Qwen3OmniMoeTextConfig:
@@ -112,10 +120,20 @@ class OmniEngineArgs(EngineArgs):
# Add the new omni-specific fields
config_dict["stage_id"] = self.stage_id
config_dict["async_chunk"] = self.async_chunk
config_dict["model_stage"] = self.model_stage
config_dict["model_arch"] = self.model_arch
config_dict["engine_output_type"] = self.engine_output_type
# Build stage_connector_config from stage_connector_spec
stage_connector_config = {
"name": self.stage_connector_spec.get("name", "SharedMemoryConnector"),
"extra": self.stage_connector_spec.get("extra", {}).copy(),
}
stage_connector_config["extra"]["stage_id"] = self.stage_id
config_dict["stage_connector_config"] = stage_connector_config
config_dict["hf_config_name"] = self.hf_config_name
config_dict["custom_process_next_stage_input_func"] = self.custom_process_next_stage_input_func
config_dict["omni_kv_config"] = self.omni_kv_config
if self.hf_config_name is not None:
config_dict["hf_text_config"] = self.draw_hf_text_config(config_dict)
@@ -140,6 +158,7 @@ class AsyncOmniEngineArgs(AsyncEngineArgs):
engine_output_type: Optional output type specification for the engine.
Used to route outputs to appropriate processors (e.g., "image",
"audio", "latents"). If None, output type is inferred.
stage_connector_spec: Extra configuration for stage connector
"""
stage_id: int = 0
@@ -147,6 +166,9 @@ class AsyncOmniEngineArgs(AsyncEngineArgs):
model_arch: str = "Qwen2_5OmniForConditionalGeneration"
engine_output_type: str | None = None
hf_config_name: str | None = None
custom_process_next_stage_input_func: str | None = None
stage_connector_spec: dict[str, any] = field(default_factory=dict)
async_chunk: bool = False
omni_kv_config: dict | None = None
def draw_hf_text_config(self, config_dict: dict) -> Qwen3OmniMoeTextConfig:
@@ -186,11 +208,19 @@ class AsyncOmniEngineArgs(AsyncEngineArgs):
# Add the new omni-specific fields
config_dict["stage_id"] = self.stage_id
config_dict["async_chunk"] = self.async_chunk
config_dict["model_stage"] = self.model_stage
config_dict["model_arch"] = self.model_arch
config_dict["engine_output_type"] = self.engine_output_type
stage_connector_config = {
"name": self.stage_connector_spec.get("name", "SharedMemoryConnector"),
"extra": self.stage_connector_spec.get("extra", {}).copy(),
}
stage_connector_config["extra"]["stage_id"] = self.stage_id
config_dict["stage_connector_config"] = stage_connector_config
config_dict["hf_config_name"] = self.hf_config_name
config_dict["custom_process_next_stage_input_func"] = self.custom_process_next_stage_input_func
config_dict["omni_kv_config"] = self.omni_kv_config
if self.hf_config_name is not None:
config_dict["hf_text_config"] = self.draw_hf_text_config(config_dict)
+7 -1
View File
@@ -111,9 +111,15 @@ class OmniRequestState(RequestState):
for k, v in self.mm_accumulated.items():
if isinstance(v, list) and v and isinstance(v[0], torch.Tensor):
try:
self.mm_accumulated[k] = torch.cat(v, dim=0)
if k == "audio":
# When the audio tensor shape is inconsistent, torch.cat will fail.
# We need to use torch.cat in -1 dimension.
self.mm_accumulated[k] = torch.cat(v, dim=-1)
else:
self.mm_accumulated[k] = torch.cat(v, dim=0)
except Exception:
# Keep last tensor on failure
logger.warning(f"Error concatenating tensor for key {k}; keeping last tensor")
self.mm_accumulated[k] = v[-1]
elif isinstance(v, dict):
for sk, sv in v.items():
+253 -127
View File
@@ -20,7 +20,7 @@ from vllm.v1.engine.exceptions import EngineDeadError
# Internal imports (our code)
from vllm_omni.config import OmniModelConfig
from vllm_omni.diffusion.data import DiffusionParallelConfig
from vllm_omni.distributed.omni_connectors.adapter import try_send_via_connector
from vllm_omni.distributed.omni_connectors.adapter import compute_talker_prompt_ids_length, try_send_via_connector
from vllm_omni.distributed.ray_utils.utils import try_close_ray
from vllm_omni.engine.input_processor import OmniInputProcessor
from vllm_omni.entrypoints.client_request_state import ClientRequestState
@@ -325,139 +325,76 @@ class AsyncOmni(OmniBase):
self._enable_stats,
_wall_start_ts,
)
# Seed stage-0 queue with all requests
logger.debug(f"[{self._name}] Seeding request into stage-0")
req_state = ClientRequestState(request_id)
req_state.metrics = metrics
self.request_states[request_id] = req_state
# Mark first input time for stage-0
metrics.stage_first_ts[0] = metrics.stage_first_ts[0] or time.time()
sp0: SamplingParams = sampling_params_list[0] # type: ignore[index]
task = {
"request_id": request_id,
"engine_inputs": prompt,
"sampling_params": sp0,
}
self.stage_list[0].submit(task)
_req_start_ts[request_id] = time.time()
logger.debug(f"[{self._name}] Enqueued request {request_id} to stage-0")
if self.async_chunk:
stage_queues = {stage_id: asyncio.Queue() for stage_id in range(num_stages)}
req_state.stage_queues = stage_queues
for i in range(num_stages):
sp: SamplingParams = sampling_params_list[i]
engine_inputs = prompt
if i != 0:
prompt_token_ids = prompt["prompt_token_ids"]
prompt_1 = prompt.copy()
prompt_1["prompt_token_ids"] = [0] * compute_talker_prompt_ids_length(prompt_token_ids)
prompt_1["multi_modal_data"] = prompt_1["mm_processor_kwargs"] = None
engine_inputs = prompt_1
logger.debug(f"[{self._name}] Entering scheduling loop: stages={num_stages}")
for stage_id, stage in enumerate(self.stage_list[: final_stage_id_for_e2e + 1]):
finished = False
while not finished:
result = await req_state.queue.get()
assert stage_id == req_state.stage_id
task = {
"request_id": request_id,
"engine_inputs": engine_inputs,
"sampling_params": sp,
}
self.stage_list[i].submit(task)
metrics.stage_first_ts[i] = metrics.stage_first_ts[0] or time.time()
req_id = result.get("request_id")
if "error" in result:
logger.error(
f"[{self._name}] Stage {stage_id} error on request {req_id}: {result['error']}",
)
raise RuntimeError(result) # Request Finished due to error
logger.info(f"[{self._name}] Enqueued request {request_id} to stage-{str(i)}")
engine_outputs = _load(result, obj_key="engine_outputs", shm_key="engine_outputs_shm")
if isinstance(engine_outputs, list):
engine_outputs = engine_outputs[0]
finished = engine_outputs.finished
_req_start_ts[request_id] = time.time()
# Mark last output time for this stage whenever we receive outputs
metrics.stage_last_ts[stage_id] = max(metrics.stage_last_ts[stage_id] or 0.0, time.time())
try:
_m = asdict(result.get("metrics"))
if _m is not None and finished:
metrics.on_stage_metrics(stage_id, req_id, _m)
except Exception as e:
logger.exception(
f"[{self._name}] Failed to process metrics for stage {stage_id}, req {req_id}: {e}",
)
logger.debug(
f"[{self._name}] Stage-{stage_id} completed request {req_id}; forwarding or finalizing",
)
logger.info(
f"[{self._name}] Entering scheduling loop: "
f"stages={num_stages}, final_stage={final_stage_id_for_e2e}"
)
async for output in self._process_async_results(
request_id,
req_state,
metrics,
final_stage_id_for_e2e,
_req_start_ts,
_wall_start_ts,
):
yield output
else:
sp0: SamplingParams = sampling_params_list[0] # type: ignore[index]
task = {
"request_id": request_id,
"engine_inputs": prompt,
"sampling_params": sp0,
}
self.stage_list[0].submit(task)
if getattr(stage, "final_output", False):
logger.debug(
f"[{self._name}] Request {req_id} finalized at stage-{stage_id}",
)
_req_start_ts[request_id] = time.time()
# Mark first input time for stage-0
metrics.stage_first_ts[0] = metrics.stage_first_ts[0] or time.time()
logger.info(
f"[{self._name}] Entering scheduling loop: "
f"stages={num_stages}, final_stage={final_stage_id_for_e2e}"
)
# End-to-end timing and time-per-token for final output
# (only once per request at the designated final stage)
try:
rid_key = str(req_id)
if stage_id == final_stage_id_for_e2e and rid_key not in metrics.e2e_done and finished:
metrics.on_finalize_request(
stage_id,
req_id,
_req_start_ts.get(req_id, _wall_start_ts),
)
except Exception as e:
logger.exception(
f"[{self._name}] Finalize request handling error for req "
f"{req_id} at stage {stage_id}: {e}",
)
# Handle diffusion outputs that already contain images
if stage.final_output_type == "image":
images = []
if isinstance(engine_outputs, OmniRequestOutput) and engine_outputs.images:
images = engine_outputs.images
elif hasattr(engine_outputs, "images") and engine_outputs.images:
images = engine_outputs.images
yield OmniRequestOutput(
stage_id=stage_id,
final_output_type=stage.final_output_type,
request_output=engine_outputs,
images=images,
)
else:
yield OmniRequestOutput(
stage_id=stage_id,
final_output_type=stage.final_output_type,
request_output=engine_outputs,
)
if not isinstance(engine_outputs, list):
engine_outputs = [engine_outputs]
stage.set_engine_outputs(engine_outputs)
# Forward to next stage if there is one
next_stage_id = stage_id + 1
if next_stage_id <= final_stage_id_for_e2e and finished:
next_stage: OmniStage = self.stage_list[next_stage_id]
next_inputs = next_stage.process_engine_inputs(self.stage_list, prompt)
sp_next: SamplingParams = sampling_params_list[next_stage_id]
# Check if we have a connector for this edge
connector_key = (str(stage_id), str(next_stage_id))
connector = self.connectors.get(connector_key)
sent_via_connector = False
if connector:
sent_via_connector = try_send_via_connector(
connector=connector,
stage_id=stage_id,
next_stage_id=next_stage_id,
req_id=req_id,
next_inputs=next_inputs,
sampling_params=sp_next,
original_prompt=prompt,
next_stage_queue_submit_fn=self.stage_list[next_stage_id].submit,
metrics=metrics,
)
if not sent_via_connector:
# Fallback logic removed as we now enforce connector usage.
# If no connector is found or send fails, we log an error and raise,
# because continuing would cause the request to be silently dropped
# and the orchestrator to hang waiting for completion.
error_msg = (
f"[{self._name}] Failed to send request {req_id} to stage-{next_stage_id} via connector. "
"Configure a connector for this edge or inspect connector logs for details."
)
logger.error(error_msg)
raise RuntimeError(error_msg)
logger.debug(f"[{self._name}] Forwarded request {req_id} to stage-{next_stage_id}")
else:
logger.debug(f"[{self._name}] Request {req_id} fully completed")
async for output in self._process_sequential_results(
request_id,
req_state,
metrics,
final_stage_id_for_e2e,
_req_start_ts,
_wall_start_ts,
sampling_params_list,
prompt,
):
yield output
logger.debug(f"[{self._name}] All requests completed")
@@ -474,6 +411,184 @@ class AsyncOmni(OmniBase):
logger.info("[AsyncOrchestrator] Request %s aborted.", request_id)
raise
async def _process_async_results(
self,
request_id: str,
req_state: ClientRequestState,
metrics: OrchestratorMetrics,
final_stage_id_for_e2e: int,
req_start_ts: dict[int, float],
wall_start_ts: float,
) -> AsyncGenerator[OmniRequestOutput, None]:
all_stages_finished = {stage_id: False for stage_id in range(final_stage_id_for_e2e + 1)}
while not all(all_stages_finished.values()):
for stage_id, stage in enumerate(self.stage_list[: final_stage_id_for_e2e + 1]):
if all_stages_finished[stage_id]:
continue
result = await req_state.stage_queues[stage_id].get()
logger.info(f"[{self._name}] Received result from stage-{stage_id}: {result}")
engine_outputs, finished, output_to_yield = self._process_single_result(
result, stage, stage_id, metrics, req_start_ts, wall_start_ts, final_stage_id_for_e2e
)
all_stages_finished[stage_id] = finished
if output_to_yield:
yield output_to_yield
async def _process_sequential_results(
self,
request_id: str,
req_state: ClientRequestState,
metrics: OrchestratorMetrics,
final_stage_id_for_e2e: int,
req_start_ts: dict[int, float],
wall_start_ts: float,
sampling_params_list: list[SamplingParams],
prompt: Any,
) -> AsyncGenerator[OmniRequestOutput, None]:
for stage_id, stage in enumerate(self.stage_list[: final_stage_id_for_e2e + 1]):
finished = False
while not finished:
result = await req_state.queue.get()
assert stage_id == req_state.stage_id
req_id = result.get("request_id")
engine_outputs, finished, output_to_yield = self._process_single_result(
result, stage, stage_id, metrics, req_start_ts, wall_start_ts, final_stage_id_for_e2e
)
if output_to_yield:
yield output_to_yield
if not isinstance(engine_outputs, list):
engine_outputs = [engine_outputs]
stage.set_engine_outputs(engine_outputs)
# Forward to next stage if there is one
next_stage_id = stage_id + 1
if next_stage_id <= final_stage_id_for_e2e and finished:
next_stage: OmniStage = self.stage_list[next_stage_id]
next_inputs = next_stage.process_engine_inputs(self.stage_list, prompt)
sp_next: SamplingParams = sampling_params_list[next_stage_id]
# Check if we have a connector for this edge
connector_key = (str(stage_id), str(next_stage_id))
connector = self.connectors.get(connector_key)
sent_via_connector = False
if connector:
sent_via_connector = try_send_via_connector(
connector=connector,
stage_id=stage_id,
next_stage_id=next_stage_id,
req_id=req_id,
next_inputs=next_inputs,
sampling_params=sp_next,
original_prompt=prompt,
next_stage_queue_submit_fn=self.stage_list[next_stage_id].submit,
metrics=metrics,
)
if not sent_via_connector:
# Fallback logic removed as we now enforce connector usage.
# If no connector is found or send fails, we log an error and raise,
# because continuing would cause the request to be silently dropped
# and the orchestrator to hang waiting for completion.
error_msg = (
f"[{self._name}] Failed to send request {req_id} to stage-{next_stage_id} via connector. "
"Configure a connector for this edge or inspect connector logs for details."
)
logger.error(error_msg)
raise RuntimeError(error_msg)
logger.debug(f"[{self._name}] Forwarded request {req_id} to stage-{next_stage_id}")
else:
logger.debug(f"[{self._name}] Request {req_id} fully completed")
def _process_single_result(
self,
result: dict[str, Any],
stage: OmniStage,
stage_id: int,
metrics: OrchestratorMetrics,
req_start_ts: dict[int, float],
wall_start_ts: float,
final_stage_id_for_e2e: int,
) -> tuple[Any, bool, OmniRequestOutput | None]:
"""
Process a single result dictionary from a stage.
Returns:
engine_outputs: The decoded outputs.
finished: Whether the stage processing is finished for this request.
output_to_yield: An OmniRequestOutput to yield, or None.
"""
req_id = result.get("request_id")
if "error" in result:
logger.error(
f"[{self._name}] Stage {stage_id} error on request {req_id}: {result['error']}",
)
raise RuntimeError(result)
engine_outputs = _load(result, obj_key="engine_outputs", shm_key="engine_outputs_shm")
if isinstance(engine_outputs, list):
engine_outputs = engine_outputs[0]
finished = engine_outputs.finished
# Mark last output time
metrics.stage_last_ts[stage_id] = max(metrics.stage_last_ts[stage_id] or 0.0, time.time())
try:
_m = asdict(result.get("metrics"))
if _m is not None and finished:
metrics.on_stage_metrics(stage_id, req_id, _m)
except Exception as e:
logger.exception(
f"[{self._name}] Failed to process metrics for stage {stage_id}, req {req_id}: {e}",
)
logger.debug(
f"[{self._name}] Stage-{stage_id} completed request {req_id}; forwarding or finalizing",
)
output_to_yield = None
if getattr(stage, "final_output", False):
logger.debug(f"[{self._name}] Request {req_id} finalized at stage-{stage_id}")
# Finalize request metrics if this is the E2E final stage and it's finished
try:
rid_key = str(req_id)
if stage_id == final_stage_id_for_e2e and rid_key not in metrics.e2e_done and finished:
metrics.on_finalize_request(
stage_id,
req_id,
req_start_ts.get(req_id, wall_start_ts),
)
except Exception as e:
logger.exception(
f"[{self._name}] Finalize request handling error for req {req_id} at stage {stage_id}: {e}",
)
# Construct output to yield
images = []
if stage.final_output_type == "image":
if isinstance(engine_outputs, OmniRequestOutput) and engine_outputs.images:
images = engine_outputs.images
elif hasattr(engine_outputs, "images") and engine_outputs.images:
images = engine_outputs.images
if stage.final_output_type == "image":
output_to_yield = OmniRequestOutput(
stage_id=stage_id,
final_output_type=stage.final_output_type,
request_output=engine_outputs,
images=images,
)
else:
output_to_yield = OmniRequestOutput(
stage_id=stage_id,
final_output_type=stage.final_output_type,
request_output=engine_outputs,
)
return engine_outputs, finished, output_to_yield
def _run_output_handler(self) -> None:
if self.output_handler is not None:
return
@@ -503,8 +618,12 @@ class AsyncOmni(OmniBase):
dropping output for req {req_id} at stage-{stage_id}"
)
continue
await req_state.queue.put(result)
req_state.stage_id = stage_id
if hasattr(req_state, "stage_queues") and stage_id in req_state.stage_queues:
await req_state.stage_queues[stage_id].put(result)
else:
# Fallback to old behavior for compatibility
await req_state.queue.put(result)
req_state.stage_id = stage_id
if idle:
await asyncio.sleep(0.001) # Avoid CPU overload when idle
else:
@@ -512,7 +631,14 @@ class AsyncOmni(OmniBase):
except Exception as e:
logger.exception("AsyncOmni output_handler failed.")
for req_state in request_states.values():
await req_state.queue.put({"request_id": req_id, "error": str(e)})
error_msg = {"request_id": req_state.request_id, "error": str(e)}
# Send error to all stage queues
if hasattr(req_state, "stage_queues"):
for queue in req_state.stage_queues.values():
await queue.put(error_msg)
else:
await req_state.queue.put(error_msg)
error_msg = {"request_id": req_state.request_id, "error": str(e)}
self.output_handler = None # Make possible for restart
self.output_handler = asyncio.create_task(output_handler())
+7
View File
@@ -232,6 +232,8 @@ class OmniBase:
self.worker_backend = worker_backend
self.ray_address = ray_address
self.batch_timeout = batch_timeout
# async chunk remains the same for each stage
self.async_chunk = self._is_async_chunk_enable(self.stage_configs)
# Build OmniStage instances in parallel, preserve original order
def _build_stage(idx_cfg: tuple[int, Any]) -> tuple[int, OmniStage]:
@@ -261,6 +263,11 @@ class OmniBase:
# Wait for all stages to report readiness before seeding
self._wait_for_stages_ready(timeout=init_timeout)
def _is_async_chunk_enable(self, stage_args: list) -> bool:
"""get async chunk flag"""
engine_args = getattr(stage_args[0], "engine_args", None)
return bool(getattr(engine_args, "async_chunk", False))
def _start_stages(self, model: str) -> None:
"""Start all stage processes."""
if self.worker_backend == "ray":
+17 -6
View File
@@ -673,12 +673,15 @@ def _stage_worker(
e,
)
# Init engine based on stage_type
logger.debug(
"[Stage-%s] Initializing %s engine with args keys=%s",
stage_id,
stage_type,
list(engine_args.keys()),
)
logger.debug("[Stage-%s] Initializing %s engine with args keys=%s", stage_id, stage_type, list(engine_args.keys()))
if engine_args.get("async_chunk", False):
logger.debug("[Stage-%s] Async chunk enabled, injecting connectors config", stage_id)
stage_connector_spec = {}
for v in connectors_config.values():
stage_connector_spec = dict(v.get("spec", {}))
break
engine_args["stage_connector_spec"] = stage_connector_spec
engine_args["stage_id"] = stage_id
try:
if stage_type == "diffusion":
engine_args.pop("model_stage", None)
@@ -1206,6 +1209,14 @@ async def _stage_worker_async(
stage_type,
list(engine_args.keys()),
)
if engine_args.get("async_chunk", False):
logger.debug("[Stage-%s] Async chunk enabled, injecting connectors config", stage_id)
stage_connector_spec = {}
for v in connectors_config.values():
stage_connector_spec = dict(v.get("spec", {}))
break
engine_args["stage_connector_spec"] = stage_connector_spec
engine_args["stage_id"] = stage_id
try:
if stage_type == "diffusion":
# For diffusion, we need to extract diffusion-specific config
+6 -1
View File
@@ -18,6 +18,7 @@ try:
except ImportError:
soundfile = None
from openai.types.chat.chat_completion_audio import ChatCompletionAudio as OpenAIChatCompletionAudio
from vllm.entrypoints.chat_utils import (
ChatCompletionMessageParam,
@@ -1213,6 +1214,7 @@ class OmniOpenAIServingChat(OpenAIServingChat, AudioMixin):
yield f"data: {data}\n\n"
elif final_output_type == "audio":
role = self.get_chat_request_role(request)
choices_data = self._create_audio_choice(omni_res, role, request, stream=True)
chunk = OmniChatCompletionStreamResponse(
id=request_id,
@@ -1655,7 +1657,10 @@ class OmniOpenAIServingChat(OpenAIServingChat, AudioMixin):
):
choices: list[ChatCompletionResponseChoice] = []
final_res = omni_outputs.request_output
audio_tensor = final_res.multimodal_output["audio"].float().detach().cpu().numpy()
if stream:
audio_tensor = final_res.multimodal_output["audio"][-1].float().detach().cpu().numpy()
else:
audio_tensor = final_res.multimodal_output["audio"].float().detach().cpu().numpy()
# Ensure audio is 1D (flatten if needed)
if audio_tensor.ndim > 1:
+2 -1
View File
@@ -222,7 +222,8 @@ def maybe_dump_to_shm(obj: Any, threshold: int) -> tuple[bool, Any]:
"""
payload = serialize_obj(obj)
if len(payload) > threshold:
return True, shm_write_bytes(payload)
logger.debug(f"Dumping object to SHM with size: {len(payload)}")
return True, shm_write_bytes(payload, name=None)
return False, obj
+2
View File
@@ -221,6 +221,7 @@ def load_stage_configs_from_yaml(config_path: str, base_engine_args: dict | None
base_engine_args = {}
config_data = OmegaConf.load(config_path)
stage_args = config_data.stage_args
global_async_chunk = config_data.get("async_chunk", False)
# Convert any nested dataclass objects to dicts before creating OmegaConf
base_engine_args = _convert_dataclasses_to_dict(base_engine_args)
base_engine_args = OmegaConf.create(base_engine_args)
@@ -234,6 +235,7 @@ def load_stage_configs_from_yaml(config_path: str, base_engine_args: dict | None
runtime_cfg = stage_arg.runtime
max_batch_size = int(runtime_cfg.get("max_batch_size", 1) or 1)
base_engine_args_tmp["max_num_seqs"] = max_batch_size
base_engine_args_tmp.async_chunk = global_async_chunk
stage_arg.engine_args = base_engine_args_tmp
return stage_args
@@ -180,6 +180,8 @@ class Qwen3OmniMoeForConditionalGeneration(
self.thinker.make_empty_intermediate_tensors if self.model_stage == "thinker" else lambda: None
)
self.chunk_segment_info = {} # request_id -> chunk_segment_info
# ==================== Device utilities ====================
@staticmethod
@@ -483,12 +485,19 @@ class Qwen3OmniMoeForConditionalGeneration(
talker_codes = talker_codes.unsqueeze(0).unsqueeze(0)
talker_codes = talker_codes.expand(1, 16, -1)
# Use chunked decode for memory efficiency
audio_tensor = self.code2wav.chunked_decode(
talker_codes,
chunk_size=300,
left_context_size=25,
)
if self.vllm_config.model_config.async_chunk:
audio_tensor = self.code2wav.chunked_decode_streaming(
talker_codes,
chunk_size=25,
left_context_size=25,
)
else:
# Use chunked decode for memory efficiency
audio_tensor = self.code2wav.chunked_decode(
talker_codes,
chunk_size=300,
left_context_size=25,
)
return audio_tensor
@@ -595,7 +604,6 @@ class Qwen3OmniMoeForConditionalGeneration(
last_talker_hidden, text_step, update_dict = self.talker_preprocess_decode(
input_ids, input_embeds, **info_dict
)
update_dict["mtp_inputs"] = last_talker_hidden, text_step
return input_ids, input_embeds, update_dict
@@ -624,6 +632,33 @@ class Qwen3OmniMoeForConditionalGeneration(
inputs_embeds = (inputs_embeds + text_step).reshape(-1, self.talker_config.text_config.hidden_size)
return inputs_embeds, code_predictor_codes.squeeze(-1)
def _get_tts_embed(self, thinker_embed, tts_bos_thinker, tts_eos_thinker, tts_pad_thinker):
"""Project thinker-side TTS embeddings into talker text space."""
module_device = self._module_device(self.talker)
def _ensure_1x1(x: torch.Tensor) -> torch.Tensor:
if x.ndim == 3:
return x[0, -1:, :]
if x.ndim == 2:
return x[-1]
return x.view(1, 1, -1)
def _proj_from_thinker(x_opt: torch.Tensor | None) -> torch.Tensor:
if isinstance(x_opt, torch.Tensor) and x_opt.numel() > 0:
xin = _ensure_1x1(x_opt).to(module_device)
else:
xin = torch.zeros(
(1, thinker_embed.shape[-1]),
device=module_device,
dtype=thinker_embed.dtype,
)
return self.talker.text_projection(xin).to(module_device)
self.tts_bos_embed = _proj_from_thinker(tts_bos_thinker)
self.tts_eos_embed = _proj_from_thinker(tts_eos_thinker)
self.tts_pad_embed = _proj_from_thinker(tts_pad_thinker)
return self.tts_bos_embed, self.tts_eos_embed, self.tts_pad_embed
def talker_preprocess_prefill(self, input_ids: torch.Tensor, input_embeds: torch.Tensor, **info_dict: dict):
# Containers to return per-request updates (e.g., code_predictor_hidden_per_request)
update_dict: dict[str, dict] = {}
@@ -647,7 +682,7 @@ class Qwen3OmniMoeForConditionalGeneration(
if info_dict.get("thinker_input_ids") is None
else torch.as_tensor(info_dict.get("thinker_input_ids"), device=self._module_device(self.talker))
)
# Optional thinker-side TTS token embeddings (to be projected by talker)
tts_bos_thinker = info_dict.get("tts_bos_embed").to(
device=self._module_device(self.talker), dtype=torch.bfloat16
)
@@ -700,7 +735,7 @@ class Qwen3OmniMoeForConditionalGeneration(
tts_pad_thinker=tts_pad_thinker,
)
# Queue tailing_text_hidden for decode (drop first for next steps),
# Queue trailing_text_hidden for decode (drop first for next steps),
try:
if isinstance(trailing_text_hidden, torch.Tensor) and trailing_text_hidden.numel() > 0:
if trailing_text_hidden.ndim == 2:
@@ -716,7 +751,7 @@ class Qwen3OmniMoeForConditionalGeneration(
# compatible with old shape [1,S,D]
rem_tail = trailing_text_hidden.squeeze(0)
if rem_tail.shape[0] > 0:
update_dict["tailing_text_hidden"] = rem_tail.detach().to("cpu").contiguous()
update_dict["trailing_text_hidden"] = rem_tail.detach().to("cpu").contiguous()
# Also persist projected tts_pad for decode fallback if needed
if isinstance(tts_pad_thinker, torch.Tensor):
pad_in = tts_pad_thinker
@@ -762,31 +797,9 @@ class Qwen3OmniMoeForConditionalGeneration(
(thinker_result_ids == self.thinker_config.video_token_id)
).to(input_ids.device) # [t] # fmt: skip
# Project thinker-side TTS embeddings into talker text space (provided by thinker stage)
talker_dev = self._module_device(self.talker)
def _ensure_1x1(x: torch.Tensor) -> torch.Tensor:
if x.ndim == 3:
return x[0, -1:, :]
if x.ndim == 2:
return x[-1]
return x.view(1, 1, -1)
def _proj_from_thinker(x_opt: torch.Tensor | None) -> torch.Tensor:
if isinstance(x_opt, torch.Tensor) and x_opt.numel() > 0:
xin = _ensure_1x1(x_opt).to(talker_dev)
else:
xin = torch.zeros(
(1, thinker_embed.shape[-1]),
device=talker_dev,
dtype=thinker_embed.dtype,
)
return self.talker.text_projection(xin).to(input_ids.device)
tts_bos_embed = _proj_from_thinker(tts_bos_thinker)
tts_eos_embed = _proj_from_thinker(tts_eos_thinker)
tts_pad_embed = _proj_from_thinker(tts_pad_thinker)
self.tts_pad_embed = tts_pad_embed
tts_bos_embed, tts_eos_embed, tts_pad_embed = self._get_tts_embed(
thinker_embed, tts_bos_thinker, tts_eos_thinker, tts_pad_thinker
)
talker_input_embeds = [] # [1 t d]
talker_input_ids = []
@@ -835,24 +848,58 @@ class Qwen3OmniMoeForConditionalGeneration(
return talker_input_id, talker_input_embed, trailing_text_hidden_all
def _thinker_decode_to_talker_decode(
self,
info_dict: dict,
device: torch.device,
update_dict,
):
"""
Project thinker outputs to talker inputs during prefill stage.
Returns:
(input_ids, input_embeds) for talker
"""
thinker_embed = info_dict.get("thinker_embeddings", None)
if thinker_embed is None:
if info_dict.get("finished_flag"):
return self.tts_pad_embed.to(device)
update_dict["finished_flag"] = True
return self.tts_eos_embed.to(device)
thinker_embed = thinker_embed.to(device)
return self.talker.text_projection(thinker_embed).to(device)
def talker_preprocess_decode(self, input_ids: torch.Tensor, input_embeds: torch.Tensor, **info_dict: dict):
update_dict: dict[str, dict] = {}
last_talker_hidden = None
text_step = None
try:
q_tail = info_dict.get("tailing_text_hidden")
if isinstance(q_tail, torch.Tensor) and q_tail.numel() > 0:
use_vec = q_tail[0:1, :]
new_q_tail = (
q_tail[1:, :].detach().to("cpu").contiguous()
if q_tail.shape[0] > 1
else self.tts_pad_embed.to(input_embeds.device, dtype=input_embeds.dtype)
)
text_step = use_vec.to(input_embeds.device, dtype=input_embeds.dtype)
update_dict["tailing_text_hidden"] = new_q_tail
if self.vllm_config.model_config.async_chunk:
text_step = self._thinker_decode_to_talker_decode(info_dict, input_ids.device, update_dict)
else:
text_step = self.tts_pad_embed.to(input_embeds.device, dtype=input_embeds.dtype)
q_tail = info_dict.get("trailing_text_hidden", None)
if isinstance(q_tail, torch.Tensor) and q_tail.numel() > 0:
use_vec = q_tail[0:1, :]
new_q_tail = (
q_tail[1:, :].detach().to("cpu").contiguous()
if q_tail.shape[0] > 1
else self.tts_pad_embed.to(input_embeds.device, dtype=input_embeds.dtype)
)
text_step = use_vec.to(input_embeds.device, dtype=input_embeds.dtype)
update_dict["trailing_text_hidden"] = new_q_tail
else:
text_step = self.tts_pad_embed.to(input_embeds.device, dtype=input_embeds.dtype)
last_talker_hidden = info_dict.get("last_talker_hidden").to(input_embeds.device, dtype=input_embeds.dtype)
last_talker_hidden = last_talker_hidden.reshape(*last_talker_hidden.shape[-2:]) # [1, hidden_size]
last_talker_hidden_tensor = info_dict.get("last_talker_hidden")
if last_talker_hidden_tensor is not None:
last_talker_hidden = last_talker_hidden_tensor.to(input_embeds.device, dtype=input_embeds.dtype)
last_talker_hidden = last_talker_hidden.reshape(*last_talker_hidden.shape[-2:]) # [1, hidden_size]
else:
last_talker_hidden = torch.zeros(
(1, self.talker_config.text_config.hidden_size),
device=input_embeds.device,
dtype=input_embeds.dtype,
)
except Exception as e:
logger.error(f"Error in decode: {e}")
@@ -866,7 +913,6 @@ class Qwen3OmniMoeForConditionalGeneration(
)
user_mm_mask = multimodal_mask[im_start_index:segment_end_index]
# Multimodal data exists
if user_mm_mask.any():
user_thinker_hidden_mm = thinker_hidden[im_start_index:segment_end_index][user_mm_mask]
@@ -883,12 +929,20 @@ class Qwen3OmniMoeForConditionalGeneration(
assistant_hidden = self.talker.text_projection(thinker_embed[im_start_index:segment_end_index]).to(
tts_pad_embed.device
) # [t, d]
# [3 tokens] + [4 pad] + [1 BOS] + [1 first text] = 9 tokens
assistant_text_hidden = torch.cat(
(
assistant_hidden[:3],
tts_pad_embed.expand(4, -1),
tts_bos_embed,
assistant_hidden[3:4], # First text
assistant_hidden[3:4]
if assistant_hidden.shape[0] > 3
else torch.zeros(
(1, assistant_hidden.shape[1]),
device=assistant_hidden.device,
dtype=assistant_hidden.dtype,
), # First text
),
dim=0,
)
@@ -904,6 +958,9 @@ class Qwen3OmniMoeForConditionalGeneration(
device=tts_pad_embed.device,
dtype=torch.long,
)
embed_input_ids = self.talker.embed_input_ids(codec_special_tokens).to(
device=tts_pad_embed.device, dtype=torch.bfloat16
)
assistant_codec_hidden = torch.cat(
(
torch.zeros(
@@ -911,18 +968,21 @@ class Qwen3OmniMoeForConditionalGeneration(
device=tts_pad_embed.device,
dtype=torch.bfloat16,
),
self.talker.embed_input_ids(codec_special_tokens).to(device=tts_pad_embed.device, dtype=torch.bfloat16),
),
dim=0,
)
trailing_text_hidden = torch.cat(
(
assistant_hidden[4:],
tts_eos_embed,
embed_input_ids,
),
dim=0,
)
if assistant_hidden.shape[0] > 4:
trailing_text_hidden = torch.cat(
(assistant_hidden[4:], tts_eos_embed),
dim=0,
)
else:
trailing_text_hidden = torch.zeros(
tts_eos_embed.shape, device=tts_eos_embed.device, dtype=tts_eos_embed.dtype
)
input_embeds = assistant_text_hidden + assistant_codec_hidden
input_ids = torch.full(
(assistant_text_hidden.shape[0],),
@@ -196,6 +196,39 @@ class Qwen3OmniMoeCode2Wav(nn.Module):
return torch.cat(wavs, dim=-1)
def chunked_decode_streaming(
self,
codes: torch.Tensor,
chunk_size: int = 25,
left_context_size: int = 25,
) -> torch.Tensor:
"""
Decode long sequences in chunks to avoid OOM.
Uses overlapping chunks with left context to avoid boundary artifacts.
Args:
codes: [batch, num_quantizers, seq_len] - num_quantizers-layer RVQ codes
chunk_size: Number of codec frames per chunk
left_context_size: Number of overlapping frames for context
Returns:
waveform: [batch, 1, waveform_len] - Complete waveform
"""
wavs = []
end_index = codes.shape[-1]
# TODO: need to optimize algorithms, current only support
# chunk_size = left_context_size = 25
if end_index == chunk_size:
context_size = 0
else:
context_size = left_context_size
# Decode chunk
wav_chunk = self(codes)
# Remove context from output (context_size * total_upsample samples)
wavs.append(wav_chunk[..., context_size * self.total_upsample :])
return torch.cat(wavs, dim=-1)
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
"""Load weights from HuggingFace checkpoint."""
loader = AutoWeightsLoader(
@@ -4,12 +4,13 @@
# Stage 2: Code2Wav (8-layer RVQ codes → audio waveform)
# The following config has been verified on 2x H100-80G GPUs.
async_chunk: false
stage_args:
- stage_id: 0
stage_type: llm # Use llm stage type to launch OmniLLM
runtime:
devices: "0,1"
max_batch_size: 1
max_batch_size: 64
engine_args:
model_stage: thinker
model_arch: Qwen3OmniMoeForConditionalGeneration
@@ -40,17 +41,16 @@ stage_args:
stage_type: llm # Use llm stage type to launch OmniLLM
runtime:
devices: "1"
max_batch_size: 1
max_batch_size: 64
engine_args:
model_stage: talker
model_arch: Qwen3OmniMoeForConditionalGeneration
worker_cls: vllm_omni.worker.gpu_ar_worker.GPUARWorker
scheduler_cls: vllm_omni.core.sched.omni_ar_scheduler.OmniARScheduler
gpu_memory_utilization: 0.35
gpu_memory_utilization: 0.3
enforce_eager: true
trust_remote_code: true
engine_output_type: latent # Output codec codes for code2wav
# tensor_parallel_size: 2
enable_prefix_caching: false
max_num_batched_tokens: 32768
distributed_executor_backend: "mp"
@@ -0,0 +1,101 @@
# Stage config for running Qwen3-Omni-MoE with 3-stage architecture
# Stage 0: Thinker (multimodal understanding + text generation)
# Stage 1: Talker (text embeddings → 16-layer RVQ codec codes)
# Stage 2: Code2Wav (16-layer RVQ codes → audio waveform)
# The following config has been verified on 2x H100-80G GPUs.
async_chunk: true
stage_args:
- stage_id: 0
stage_type: llm # Use llm stage type to launch OmniLLM
runtime:
devices: "0,1"
max_batch_size: 64
engine_args:
model_stage: thinker
model_arch: Qwen3OmniMoeForConditionalGeneration
worker_cls: vllm_omni.worker.gpu_ar_worker.GPUARWorker
scheduler_cls: vllm_omni.core.sched.omni_ar_scheduler.OmniARScheduler
gpu_memory_utilization: 0.6
enforce_eager: false
trust_remote_code: true
engine_output_type: latent # Output hidden states for talker
distributed_executor_backend: "mp"
enable_prefix_caching: false
max_num_batched_tokens: 32768
hf_config_name: thinker_config
tensor_parallel_size: 2
custom_process_next_stage_input_func: vllm_omni.model_executor.stage_input_processors.qwen3_omni.thinker2talker_async_chunk
final_output: true
final_output_type: text
is_comprehension: true
default_sampling_params:
temperature: 0.4
top_p: 0.9
top_k: 1
max_tokens: 2048
seed: 42
detokenize: True
repetition_penalty: 1.05
- stage_id: 1
stage_type: llm # Use llm stage type to launch OmniLLM
runtime:
devices: "0"
max_batch_size: 64
engine_args:
model_stage: talker
model_arch: Qwen3OmniMoeForConditionalGeneration
worker_cls: vllm_omni.worker.gpu_ar_worker.GPUARWorker
scheduler_cls: vllm_omni.core.sched.omni_ar_scheduler.OmniARScheduler
gpu_memory_utilization: 0.3
enforce_eager: false
trust_remote_code: true
engine_output_type: latent # Output codec codes for code2wav
enable_prefix_caching: false
max_num_batched_tokens: 32768
distributed_executor_backend: "mp"
hf_config_name: talker_config
custom_process_next_stage_input_func: vllm_omni.model_executor.stage_input_processors.qwen3_omni.talker2code2wav_async_chunk
engine_input_source: [0]
# final_output: true
# final_output_type: text
default_sampling_params:
temperature: 0.9
top_k: 50
max_tokens: 4096
seed: 42
detokenize: False
repetition_penalty: 1.05
stop_token_ids: [2150]
- stage_id: 2
stage_type: llm # Use llm stage type to launch OmniLLM
runtime:
devices: "1"
max_batch_size: 1
engine_args:
model_stage: code2wav
model_arch: Qwen3OmniMoeForConditionalGeneration
worker_cls: vllm_omni.worker.gpu_generation_worker.GPUGenerationWorker
scheduler_cls: vllm_omni.core.sched.omni_generation_scheduler.OmniGenerationScheduler
enforce_eager: true
trust_remote_code: true
async_scheduling: false
enable_prefix_caching: false
engine_output_type: audio # Final output: audio waveform
gpu_memory_utilization: 0.1
distributed_executor_backend: "mp"
max_num_batched_tokens: 10000
hf_config_name: thinker_config
engine_input_source: [1]
final_output: true
final_output_type: audio
default_sampling_params:
temperature: 0.0
top_p: 1.0
top_k: -1
max_tokens: 65536
seed: 42
detokenize: True
repetition_penalty: 1.1
@@ -9,6 +9,7 @@ import torch
from vllm.inputs import TextPrompt
from vllm.platforms import current_platform
from vllm_omni.engine import OmniEngineCoreRequest
from vllm_omni.inputs.data import OmniTokensPrompt
@@ -48,6 +49,74 @@ def _compute_talker_prompt_ids_length(info, device: torch.device | str = "cuda")
return sum_user_len + assistant_len
# =========================
# Common helpers
# =========================
def _ensure_list(x):
"""Convert ConstantList / tensor-like to Python list."""
if hasattr(x, "_x"):
return list(x._x)
elif not isinstance(x, list):
return x
return list(x)
def _validate_stage_inputs(stage_list, engine_input_source):
if not engine_input_source:
raise ValueError("engine_input_source cannot be empty")
stage_id = engine_input_source[0]
if stage_id >= len(stage_list):
raise IndexError(f"Invalid stage_id: {stage_id}")
stage = stage_list[stage_id]
if stage.engine_outputs is None:
raise RuntimeError(f"Stage {stage_id} has no outputs yet")
return stage.engine_outputs
# =========================
# Thinker -> Talker
# =========================
def thinker2talker_async_chunk(
pooling_output: dict[str, Any],
request: OmniEngineCoreRequest,
) -> list[dict[str, Any]]:
"""
Process thinker outputs to create talker inputs.
1. thinker's text generation outputs (token IDs + hidden states)
2. Split hidden states into: prompt embeddings + generated embeddings
3. Package for talker with additional information
"""
all_token_ids = request.all_token_ids # prefill + decode
prompt_token_ids = request.prompt_token_ids
# Convert ConstantList to regular list for OmniSerializer serialization
all_token_ids = _ensure_list(all_token_ids)
prompt_token_ids = _ensure_list(prompt_token_ids)
thinker_output = pooling_output
talker_additional_info = {
"thinker_embeddings": thinker_output.get("0").detach().cpu(),
"thinker_hidden_states": thinker_output.get("24").detach().cpu(),
"thinker_sequences": all_token_ids,
"thinker_input_ids": prompt_token_ids,
# Provide thinker-side TTS token embeddings for talker projection
"tts_bos_embed": thinker_output.get("tts_bos_embed").detach().cpu(),
"tts_eos_embed": thinker_output.get("tts_eos_embed").detach().cpu(),
"tts_pad_embed": thinker_output.get("tts_pad_embed").detach().cpu(),
"finished": torch.tensor(request.is_finished(), dtype=torch.bool),
}
return talker_additional_info
def thinker2talker(
stage_list: list[Any],
engine_input_source: list[int],
@@ -71,47 +140,33 @@ def thinker2talker(
Returns:
List of OmniTokensPrompt for talker stage
"""
if not engine_input_source:
raise ValueError("engine_input_source cannot be empty")
source_stage_id = engine_input_source[0]
if source_stage_id >= len(stage_list):
raise IndexError(f"Invalid stage_id: {source_stage_id}")
if stage_list[source_stage_id].engine_outputs is None:
raise RuntimeError(f"Stage {source_stage_id} has no outputs yet")
thinker_outputs = stage_list[source_stage_id].engine_outputs
talker_inputs = []
thinker_outputs = _validate_stage_inputs(stage_list, engine_input_source)
talker_inputs: list[OmniTokensPrompt] = []
device = torch.device(current_platform.device_type)
# Process each thinker output
for i, thinker_output in enumerate(thinker_outputs):
for thinker_output in thinker_outputs:
output = thinker_output.outputs[0]
thinker_embeddings = output.multimodal_output["0"].detach().to(device=device, dtype=torch.float)
thinker_hidden_states = output.multimodal_output["24"].detach().to(device=device, dtype=torch.float)
info = {
"thinker_embeddings": thinker_embeddings,
"thinker_hidden_states": thinker_hidden_states,
"thinker_sequences": thinker_output.prompt_token_ids
+ output.token_ids, # the thinker_sequences is the whole ids
"thinker_embeddings": output.multimodal_output["0"].detach().to(device=device, dtype=torch.float),
"thinker_hidden_states": output.multimodal_output["24"].detach().to(device=device, dtype=torch.float),
"thinker_sequences": (
thinker_output.prompt_token_ids + output.token_ids
), # the thinker_sequences is the whole ids
"thinker_input_ids": thinker_output.prompt_token_ids,
# Provide thinker-side TTS token embeddings for talker projection
"tts_bos_embed": (
output.multimodal_output.get("tts_bos_embed").detach().to(device=device, dtype=torch.float)
),
"tts_eos_embed": (
output.multimodal_output.get("tts_eos_embed").detach().to(device=device, dtype=torch.float)
),
"tts_pad_embed": (
output.multimodal_output.get("tts_pad_embed").detach().to(device=device, dtype=torch.float)
),
"tts_bos_embed": output.multimodal_output["tts_bos_embed"].detach().to(device=device, dtype=torch.float),
"tts_eos_embed": output.multimodal_output["tts_eos_embed"].detach().to(device=device, dtype=torch.float),
"tts_pad_embed": output.multimodal_output["tts_pad_embed"].detach().to(device=device, dtype=torch.float),
}
prompt_len = _compute_talker_prompt_ids_length(info, device=device)
talker_inputs.append(
OmniTokensPrompt(
prompt_token_ids=[0] * _compute_talker_prompt_ids_length(info, device=device),
prompt_token_ids=[0] * prompt_len,
additional_information=info,
multi_modal_data=None,
mm_processor_kwargs=None,
@@ -121,6 +176,50 @@ def thinker2talker(
return talker_inputs
# =========================
# Talker -> Code2Wav
# =========================
def talker2code2wav_async_chunk(
pooling_output: dict[str, Any],
request: OmniEngineCoreRequest,
):
"""
Pooling version.
"""
if "code_predictor_codes" not in pooling_output:
return []
code_predictor_codes = pooling_output["code_predictor_codes"]
if code_predictor_codes is None:
return []
if isinstance(code_predictor_codes, torch.Tensor):
if code_predictor_codes.numel() == 0:
return []
elif hasattr(code_predictor_codes, "__len__"):
if len(code_predictor_codes) == 0:
return []
if isinstance(code_predictor_codes, torch.Tensor):
if not code_predictor_codes.any():
return []
else:
code_tensor = torch.tensor(code_predictor_codes, dtype=torch.long)
if not code_tensor.any():
return []
codec_codes = code_predictor_codes.to(torch.long).transpose(0, 1).cpu().to(torch.long).reshape(-1).tolist()
if sum(codec_codes) == 0:
return []
return {
"code_predictor_codes": codec_codes,
"finished": torch.tensor(request.is_finished(), dtype=torch.bool),
}
def talker2code2wav(
stage_list: list[Any],
engine_input_source: list[int],
@@ -144,21 +243,10 @@ def talker2code2wav(
Returns:
List of OmniTokensPrompt for code2wav stage
"""
if not engine_input_source:
raise ValueError("engine_input_source cannot be empty")
source_stage_id = engine_input_source[0]
if source_stage_id >= len(stage_list):
raise IndexError(f"Invalid stage_id: {source_stage_id}")
if stage_list[source_stage_id].engine_outputs is None:
raise RuntimeError(f"Stage {source_stage_id} has no outputs yet")
talker_outputs = stage_list[source_stage_id].engine_outputs
code2wav_inputs = []
talker_outputs = _validate_stage_inputs(stage_list, engine_input_source)
code2wav_inputs: list[OmniTokensPrompt] = []
# Process each talker output
for i, talker_output in enumerate(talker_outputs):
for talker_output in talker_outputs:
output = talker_output.outputs[0]
seq_len = len(output.token_ids) - 1
# Extract codec codes from talker output
+5 -3
View File
@@ -280,7 +280,6 @@ class GPUARModelRunner(OmniGPUModelRunner):
aux_hidden_states = None
hidden_states, multimodal_outputs = self.extract_multimodal_outputs(model_output)
if multimodal_outputs is not None:
keys_or_type = (
list(multimodal_outputs.keys())
@@ -501,7 +500,9 @@ class GPUARModelRunner(OmniGPUModelRunner):
dtype=np.int32,
)
self._process_additional_information_updates(hidden_states, multimodal_outputs, num_scheduled_tokens_np)
self._process_additional_information_updates(
hidden_states, multimodal_outputs, num_scheduled_tokens_np, scheduler_output
)
pooler_output: list[dict[str, object]] = []
for rid in req_ids_output_copy:
@@ -761,8 +762,9 @@ class GPUARModelRunner(OmniGPUModelRunner):
for attempt in range(max_retries):
try:
put_key = f"omni_{from_stage}_to_{to_stage}_{request_id}"
success, size, metadata = connector.put(
from_stage=from_stage, to_stage=to_stage, request_id=request_id, data=data
from_stage=from_stage, to_stage=to_stage, put_key=put_key, data=data
)
if success:
return success, size, metadata
@@ -50,6 +50,17 @@ class GPUGenerationModelRunner(OmniGPUModelRunner):
- Executes generation process and returns tensors via `pooler_output`.
"""
def _update_request_states(self, scheduler_output: SchedulerOutput):
cached_reqs = scheduler_output.scheduled_cached_reqs
for _, req_id in enumerate(cached_reqs.req_ids):
req_state = self.requests.get(req_id)
assert req_state is not None
req_state.prompt_token_ids = cached_reqs.prompt_token_ids.get(req_id)
self.input_batch.remove_request(req_id)
# update the request state in self.input_batch
self.input_batch.add_request(req_state)
self._init_mrope_positions(req_state)
@torch.inference_mode()
def execute_model(
self,
@@ -74,6 +85,8 @@ class GPUGenerationModelRunner(OmniGPUModelRunner):
record_function_or_nullcontext("gpu_model_runner: preprocess"),
self.synchronize_input_prep(),
):
if self.model_config.async_chunk:
self._update_request_states(scheduler_output)
self._update_states(scheduler_output)
if not scheduler_output.total_num_scheduled_tokens:
return EMPTY_MODEL_RUNNER_OUTPUT
+50 -7
View File
@@ -808,6 +808,7 @@ class OmniGPUModelRunner(GPUModelRunner):
hidden_states: torch.Tensor,
multimodal_outputs: object,
num_scheduled_tokens_np: np.ndarray,
scheduler_output: "SchedulerOutput",
) -> None:
"""Process model-provided per-request additional_information updates and merge into request state."""
try:
@@ -815,10 +816,13 @@ class OmniGPUModelRunner(GPUModelRunner):
# TODO(Peiqi): do we have a more elegant way to do this?
if hasattr(self.model, "has_postprocess") and self.model.has_postprocess:
for req_index, req_id in enumerate(self.input_batch.req_ids):
req_state = self.requests.get(req_id)
req_infos = (
getattr(req_state, "additional_information_cpu", None) if req_state is not None else None
)
if self.model_config.async_chunk:
req_infos = self._get_additional_information(scheduler_output, req_id)
else:
req_state = self.requests.get(req_id)
req_infos = (
getattr(req_state, "additional_information_cpu", None) if req_state is not None else None
)
start_offset = int(self.query_start_loc.cpu[req_index])
sched_tokens = int(num_scheduled_tokens_np[req_index])
s, e = start_offset, start_offset + sched_tokens
@@ -859,6 +863,40 @@ class OmniGPUModelRunner(GPUModelRunner):
start_offset = int(self.query_start_loc.cpu[req_index])
self.inputs_embeds[start_offset : start_offset + overlay_len].copy_(src)
def _get_additional_information(self, scheduler_output: "SchedulerOutput", req_id: str) -> dict:
req_infos = None
req_state = self.requests.get(req_id)
additional_information_cpu = getattr(req_state, "additional_information_cpu", None)
for new_req in scheduler_output.scheduled_new_reqs:
if new_req.req_id == req_id:
payload_info = getattr(new_req, "additional_information", None)
if payload_info is not None:
return payload_info
if hasattr(scheduler_output.scheduled_cached_reqs, "additional_information"):
cached_infos = getattr(scheduler_output.scheduled_cached_reqs, "additional_information", {})
if isinstance(cached_infos, dict) and req_id in cached_infos:
req_infos = cached_infos[req_id]
if not isinstance(req_infos, dict):
req_infos = None
if req_infos is None or req_infos.get("last_talker_hidden", None) is None:
if req_infos is None:
additional_information_cpu.pop("thinker_embeddings", None)
req_infos = additional_information_cpu
else:
req_infos["last_talker_hidden"] = additional_information_cpu.get("last_talker_hidden", None)
req_infos["num_processed_thinker_tokens"] = additional_information_cpu.get(
"num_processed_thinker_tokens", 0
)
if not isinstance(req_infos, dict):
req_infos = None
if req_infos is None:
logger.warning(f"No additional_information found for req_id: {req_id}")
return req_infos
def _preprocess(
self,
scheduler_output: "SchedulerOutput",
@@ -974,9 +1012,14 @@ class OmniGPUModelRunner(GPUModelRunner):
# collect additional_information (tensor/list) for prefill portion only
decode_req_ids = []
for req_index, req_id in enumerate(self.input_batch.req_ids):
req_state = self.requests.get(req_id)
req_infos = getattr(req_state, "additional_information_cpu", None) if req_state is not None else None
# Try to get additional_information from multiple sources
if self.vllm_config.model_config.async_chunk:
req_infos = self._get_additional_information(scheduler_output, req_id)
else:
req_state = self.requests.get(req_id)
req_infos = (
getattr(req_state, "additional_information_cpu", None) if req_state is not None else None
)
start_offset = int(self.query_start_loc.cpu[req_index])
sched_tokens = int(num_scheduled_tokens_np[req_index])
s, e = start_offset, start_offset + sched_tokens