Signed-off-by: chickeyton <ngton2014@gmail.com> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
424 lines
18 KiB
Python
424 lines
18 KiB
Python
# SPDX-License-Identifier: Apache-2.0
|
|
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
|
import time
|
|
from collections.abc import Callable
|
|
from typing import Any
|
|
|
|
from vllm.v1.request import Request
|
|
|
|
from vllm_omni.metrics import OrchestratorAggregator
|
|
|
|
from .utils.logging import get_connector_logger
|
|
|
|
# Upper bound on a previous streaming chunk's generation reserve: a duplex
|
|
# turn-end chunk may retain more codec ids than the steady-state per-unit
|
|
# reserve (see MINICPMO45_DUPLEX_TURN_END_CODEC_TOKENS).
|
|
_MAX_PREVIOUS_CHUNK_GENERATION_RESERVE = 26 * 4
|
|
|
|
logger = get_connector_logger(__name__)
|
|
|
|
|
|
def try_send_via_connector(
|
|
connector: Any,
|
|
stage_id: int,
|
|
next_stage_id: int,
|
|
req_id: str,
|
|
next_inputs: Any,
|
|
sampling_params: Any,
|
|
original_prompt: Any,
|
|
next_stage_queue_submit_fn: Callable[[dict[str, Any]], None],
|
|
metrics: OrchestratorAggregator,
|
|
) -> bool:
|
|
"""
|
|
Attempts to send data via OmniConnector.
|
|
Returns True if successful, False otherwise.
|
|
Encapsulates the logic of preparing payload, sending via connector,
|
|
sending notification, and recording metrics.
|
|
"""
|
|
try:
|
|
t0 = time.time()
|
|
|
|
# Strip non-serializable multimodal feature fields from original_prompt
|
|
# before including it in metadata. After stage-0 runs, the TokPrompt
|
|
# returned by render_chat_async may carry processed multimodal features
|
|
# (mm_kwargs, mm_placeholders, mm_hashes) that contain MultiModalKwargsItems
|
|
# objects, which are not supported by OmniMsgpackEncoder. The receiving
|
|
# side (try_recv_via_connector) only extracts "engine_inputs" from the
|
|
# payload and never uses "original_prompt", so stripping these fields
|
|
# only affects debug metadata and is safe.
|
|
_MM_FEATURE_KEYS = frozenset({"mm_kwargs", "mm_placeholders", "mm_hashes"})
|
|
if isinstance(original_prompt, dict) and any(k in original_prompt for k in _MM_FEATURE_KEYS):
|
|
safe_prompt = {k: v for k, v in original_prompt.items() if k not in _MM_FEATURE_KEYS}
|
|
else:
|
|
safe_prompt = original_prompt
|
|
|
|
# Prepare data for connector
|
|
payload_data = {
|
|
"engine_inputs": next_inputs,
|
|
"sampling_params": sampling_params,
|
|
"metadata": {
|
|
"original_prompt": safe_prompt,
|
|
"stage_transition": f"{stage_id}->{next_stage_id}",
|
|
"timestamp": time.time(),
|
|
},
|
|
}
|
|
|
|
# Send data via connector
|
|
success, serialized_size, metadata = connector.put(str(stage_id), str(next_stage_id), str(req_id), payload_data)
|
|
|
|
if success:
|
|
# Send lightweight notification via queue
|
|
notify_payload = {
|
|
"type": "generate",
|
|
"request_id": req_id,
|
|
"sampling_params": sampling_params,
|
|
"from_connector": True,
|
|
"from_stage": str(stage_id),
|
|
"to_stage": str(next_stage_id),
|
|
"sent_ts": time.time(),
|
|
}
|
|
# Merge connector metadata (e.g. shm handle or inline data) into queue payload
|
|
if metadata:
|
|
notify_payload["connector_metadata"] = metadata
|
|
|
|
next_stage_queue_submit_fn(notify_payload)
|
|
|
|
t1 = time.time()
|
|
tx_ms = (t1 - t0) * 1000.0
|
|
|
|
metrics.on_forward(
|
|
stage_id,
|
|
next_stage_id,
|
|
req_id,
|
|
serialized_size, # Use size from connector
|
|
float(tx_ms),
|
|
True, # Mark as using connector
|
|
)
|
|
return True
|
|
else:
|
|
# If put returned False, we let the caller handle fallback
|
|
return False
|
|
|
|
except Exception as e:
|
|
logger.warning(
|
|
"[Orchestrator] OmniConnector failed for req %s: %s; falling back to queue",
|
|
req_id,
|
|
e,
|
|
)
|
|
return False
|
|
|
|
|
|
def try_recv_via_connector(
|
|
task: dict[str, Any],
|
|
connectors: dict[Any, Any],
|
|
stage_id: int,
|
|
) -> tuple[Any, dict[str, Any] | None]:
|
|
"""
|
|
Attempts to resolve input data from either connector or IPC.
|
|
Returns (engine_inputs, rx_metrics) or (None, None) if failed/skipped.
|
|
"""
|
|
rid = task["request_id"]
|
|
|
|
if task.get("from_connector"):
|
|
from_stage = task.get("from_stage")
|
|
to_stage = str(stage_id)
|
|
|
|
if not from_stage:
|
|
logger.error(
|
|
"[Stage-%s] 'from_connector' is true but 'from_stage' is missing for request %s", stage_id, rid
|
|
)
|
|
return None, None
|
|
|
|
# Get connector for this edge
|
|
connector_key = (from_stage, to_stage)
|
|
connector = connectors.get(connector_key)
|
|
|
|
if connector:
|
|
try:
|
|
# Get data from connector with timeout
|
|
_t_start = time.time()
|
|
connector_metadata = task.get("connector_metadata")
|
|
payload = connector.get(from_stage, to_stage, str(rid), metadata=connector_metadata)
|
|
_t_end = time.time()
|
|
|
|
if payload:
|
|
if isinstance(payload, tuple):
|
|
payload_data, serialized_size = payload
|
|
else:
|
|
payload_data = payload
|
|
serialized_size = len(connector.serialize_obj(payload_data))
|
|
else:
|
|
payload_data = None
|
|
serialized_size = 0
|
|
|
|
if payload_data and isinstance(payload_data, dict):
|
|
ein = payload_data.get("engine_inputs")
|
|
decode_ms = (_t_end - _t_start) * 1000.0
|
|
|
|
rx_metrics = {"rx_decode_time_ms": decode_ms, "rx_transfer_bytes": serialized_size}
|
|
return ein, rx_metrics
|
|
else:
|
|
logger.error(
|
|
"[Stage-%s] Failed to get data from connector for request %s or payload is empty", stage_id, rid
|
|
)
|
|
return None, None
|
|
except Exception as e:
|
|
logger.error("[Stage-%s] Error retrieving data from connector for request %s: %s", stage_id, rid, e)
|
|
return None, None
|
|
else:
|
|
logger.error(
|
|
"[Stage-%s] No connector found for edge %s -> %s for request %s", stage_id, from_stage, to_stage, rid
|
|
)
|
|
return None, None
|
|
else:
|
|
# Data comes from queue as usual (e.g. seed request for Stage-0)
|
|
# Since fallback logic is deprecated, we assume this is a direct inputs payload.
|
|
# We still need to decode it if it used SHM (via legacy stage_utils logic, or new shm_connector format)
|
|
# For Stage-0 specifically, 'engine_inputs' is often directly in the task dict.
|
|
|
|
# Try to use the new stage_utils which uses OmniSerializer
|
|
from vllm_omni.entrypoints.stage_utils import maybe_load_from_ipc_with_metrics
|
|
|
|
try:
|
|
ein, metrics = maybe_load_from_ipc_with_metrics(task, "engine_inputs", "engine_inputs_shm")
|
|
# If metrics are empty or zero, we might want to populate dummy metrics
|
|
return ein, metrics
|
|
except Exception:
|
|
# If engine_inputs is missing, it might be a different kind of payload,
|
|
# 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 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
|
|
|
|
|
|
def construct_next_stage_streaming_input_prompt(
|
|
payload_data: dict[str, Any],
|
|
request: Request,
|
|
*,
|
|
max_model_len: int | None = None,
|
|
previous_condition_len: int | None = None,
|
|
previous_condition_seq: int | None = None,
|
|
condition_seq: int | None = None,
|
|
recompute_previous_chunks: int = 0,
|
|
recompute_on_capacity: bool = False,
|
|
) -> bool:
|
|
"""Update a downstream streaming request prompt from connector payload.
|
|
|
|
Async-chunk downstream stages are prewarmed before the real Talker prompt is
|
|
known. When a Thinker payload carries ``ids.prompt`` or declares the
|
|
placeholder length in ``meta.next_stage_prompt_len``, this helper:
|
|
|
|
* Preserves ``num_computed_tokens`` while extending a non-window prompt.
|
|
Explicit replacements and one-condition window recomputes reset it. A
|
|
full-attention Talker recomputes only when the next condition would
|
|
exceed its context limit.
|
|
* Moves already-computed output tokens into ``prompt_token_ids``.
|
|
* Appends a placeholder slice using the declared downstream prompt length,
|
|
or derives its length from ``ids.prompt`` for legacy producers.
|
|
* Refreshes block hashes so the scheduler allocates KV slots for the
|
|
extended prompt without discarding prior computed state.
|
|
"""
|
|
ids = payload_data.get("ids")
|
|
if not isinstance(ids, dict):
|
|
ids = {}
|
|
payload_data["ids"] = ids
|
|
meta = payload_data.get("meta")
|
|
if not isinstance(meta, dict):
|
|
meta = {}
|
|
payload_data["meta"] = meta
|
|
# This flag is transported through a merge-based runtime buffer. Set it on
|
|
# every condition so a prior rollover cannot be replayed by a later append.
|
|
meta["streaming_prompt_recompute"] = False
|
|
next_stage_prompt_len = meta.get("next_stage_prompt_len")
|
|
if "next_stage_generation_tokens" in meta:
|
|
next_generation_tokens = meta["next_stage_generation_tokens"]
|
|
if (
|
|
not isinstance(next_generation_tokens, int)
|
|
or isinstance(next_generation_tokens, bool)
|
|
or next_generation_tokens <= 0
|
|
):
|
|
raise ValueError("next_stage_generation_tokens must be a positive integer")
|
|
generation_reserve = next_generation_tokens
|
|
else:
|
|
generation_reserve = 0
|
|
capacity_limit = (
|
|
max_model_len
|
|
if isinstance(max_model_len, int) and not isinstance(max_model_len, bool) and max_model_len > 0
|
|
else None
|
|
)
|
|
managed_prompt_len: int | None = None
|
|
if generation_reserve > 0 and capacity_limit is not None:
|
|
if (
|
|
not isinstance(next_stage_prompt_len, int)
|
|
or isinstance(next_stage_prompt_len, bool)
|
|
or next_stage_prompt_len <= 0
|
|
):
|
|
raise ValueError("capacity-managed streaming prompt requires a positive next_stage_prompt_len")
|
|
managed_prompt_len = next_stage_prompt_len
|
|
if managed_prompt_len + generation_reserve > capacity_limit:
|
|
raise ValueError(
|
|
"fresh streaming prompt plus generation reserve exceeds max_model_len: "
|
|
f"prompt={managed_prompt_len}, reserve={generation_reserve}, limit={capacity_limit}"
|
|
)
|
|
explicit_replacement = meta.get("replace_streaming_prompt") is True
|
|
if isinstance(recompute_previous_chunks, bool) or recompute_previous_chunks not in (0, 1):
|
|
raise ValueError("streaming prompt recompute supports exactly one previous chunk")
|
|
if not isinstance(recompute_on_capacity, bool):
|
|
raise ValueError("recompute_on_capacity must be a bool")
|
|
has_window_contract = recompute_previous_chunks == 1
|
|
if recompute_on_capacity and not has_window_contract:
|
|
raise ValueError("capacity-triggered streaming prompt recompute requires window_size=1")
|
|
if (
|
|
recompute_on_capacity
|
|
and not explicit_replacement
|
|
and previous_condition_seq is not None
|
|
and generation_reserve == 0
|
|
):
|
|
raise ValueError("capacity-triggered streaming prompt recompute requires a positive generation reserve")
|
|
exceeds_accumulated_capacity = (
|
|
managed_prompt_len is not None
|
|
and capacity_limit is not None
|
|
and request.num_computed_tokens + managed_prompt_len + generation_reserve > capacity_limit
|
|
)
|
|
window_recompute = (
|
|
not explicit_replacement
|
|
and has_window_contract
|
|
and previous_condition_seq is not None
|
|
and (exceeds_accumulated_capacity if recompute_on_capacity else True)
|
|
)
|
|
if not explicit_replacement and exceeds_accumulated_capacity and not window_recompute:
|
|
raise ValueError("capacity-managed streaming prompt rollover requires window_size=1")
|
|
replacement_prompt_len = next_stage_prompt_len
|
|
if window_recompute:
|
|
if managed_prompt_len is None or capacity_limit is None:
|
|
raise ValueError("streaming prompt window requires a positive generation reserve and max_model_len")
|
|
if (
|
|
not isinstance(previous_condition_len, int)
|
|
or isinstance(previous_condition_len, bool)
|
|
or previous_condition_len <= 0
|
|
or not isinstance(previous_condition_seq, int)
|
|
or isinstance(previous_condition_seq, bool)
|
|
or previous_condition_seq < 0
|
|
or not isinstance(condition_seq, int)
|
|
or isinstance(condition_seq, bool)
|
|
or condition_seq < 0
|
|
):
|
|
raise ValueError("streaming prompt recompute requires condition lengths and monotonic sequence metadata")
|
|
if condition_seq != previous_condition_seq + 1:
|
|
raise ValueError(
|
|
"streaming prompt recompute skipped a Talker condition: "
|
|
f"previous={previous_condition_seq}, current={condition_seq}"
|
|
)
|
|
|
|
num_placeholders = int(getattr(request, "num_output_placeholders", 0) or 0)
|
|
confirmed_num_computed_tokens = max(0, int(request.num_computed_tokens) - num_placeholders)
|
|
if confirmed_num_computed_tokens < request.num_prompt_tokens:
|
|
raise ValueError(
|
|
"confirmed streaming output ends before the current condition: "
|
|
f"confirmed={confirmed_num_computed_tokens}, prompt={request.num_prompt_tokens}"
|
|
)
|
|
previous_codec_ids = list(request._all_token_ids[request.num_prompt_tokens : confirmed_num_computed_tokens])
|
|
# The previous chunk may have been a turn-end unit that ran to the
|
|
# Talker's EOS with a larger reserve than this chunk's.
|
|
max_confirmed_codec_tokens = max(generation_reserve, _MAX_PREVIOUS_CHUNK_GENERATION_RESERVE) - 1
|
|
if len(previous_codec_ids) > max_confirmed_codec_tokens:
|
|
raise ValueError(
|
|
"streaming prompt recompute retained too many codec tokens: "
|
|
f"retained={len(previous_codec_ids)}, limit={max_confirmed_codec_tokens}"
|
|
)
|
|
|
|
replacement_prompt_len = previous_condition_len + len(previous_codec_ids) + managed_prompt_len
|
|
if replacement_prompt_len + generation_reserve > capacity_limit:
|
|
raise ValueError(
|
|
"sliding streaming prompt plus generation reserve exceeds max_model_len: "
|
|
f"previous={previous_condition_len}, codec={len(previous_codec_ids)}, "
|
|
f"current={managed_prompt_len}, reserve={generation_reserve}, limit={capacity_limit}"
|
|
)
|
|
ids["streaming_prompt_previous_codes"] = previous_codec_ids
|
|
meta.update(
|
|
{
|
|
"streaming_prompt_recompute": True,
|
|
"streaming_condition_seq": condition_seq,
|
|
}
|
|
)
|
|
logger.debug(
|
|
"Recomputing a one-chunk streaming prompt window: previous=%d, codec=%d, current=%d, reserve=%d, limit=%d",
|
|
previous_condition_len,
|
|
len(previous_codec_ids),
|
|
managed_prompt_len,
|
|
generation_reserve,
|
|
capacity_limit,
|
|
)
|
|
|
|
replace_prompt = explicit_replacement or window_recompute
|
|
if replace_prompt and isinstance(replacement_prompt_len, int) and replacement_prompt_len > 0:
|
|
# Some downstream stages consume complete, independently conditioned
|
|
# segments instead of extending an existing KV prefix. The producer
|
|
# declares that transport behavior explicitly in payload metadata.
|
|
new_prompt = [0] * replacement_prompt_len
|
|
request._output_token_ids.clear()
|
|
request._all_token_ids.clear()
|
|
request._all_token_ids.extend(new_prompt)
|
|
request.prompt_token_ids = new_prompt
|
|
request.num_computed_tokens = 0
|
|
request.num_prompt_tokens = replacement_prompt_len
|
|
request.update_block_hashes()
|
|
return True
|
|
prompt_token_ids = ids.get("prompt", None)
|
|
has_declared_prompt_len = (
|
|
isinstance(next_stage_prompt_len, int)
|
|
and not isinstance(next_stage_prompt_len, bool)
|
|
and next_stage_prompt_len > 0
|
|
)
|
|
if not has_declared_prompt_len and not prompt_token_ids:
|
|
return False
|
|
num_computed_tokens = request.num_computed_tokens
|
|
kept_output_tokens = request._all_token_ids[request.num_prompt_tokens : num_computed_tokens]
|
|
del request._all_token_ids[num_computed_tokens:]
|
|
request._output_token_ids.clear()
|
|
assert request.prompt_token_ids is not None
|
|
# Extend prompt with kept output tokens.
|
|
request.prompt_token_ids.extend(kept_output_tokens)
|
|
if has_declared_prompt_len:
|
|
next_prompt_len = next_stage_prompt_len
|
|
else:
|
|
next_prompt_len = max(1, compute_talker_prompt_ids_length(prompt_token_ids))
|
|
new_prompt = [0] * next_prompt_len
|
|
request._all_token_ids.extend(new_prompt or ())
|
|
request.prompt_token_ids.extend(new_prompt or ())
|
|
request.update_block_hashes()
|
|
request.num_prompt_tokens = len(request.prompt_token_ids)
|
|
return False
|