Signed-off-by: Sysy <1370724210@qq.com> Co-authored-by: Yueqian Lin <70319226+linyueqian@users.noreply.github.com>
250 lines
9.3 KiB
Python
250 lines
9.3 KiB
Python
"""Utilities for handling multimodal outputs / building multimodal output
|
|
payloads, most of which are shared by the prefix cache / no prefix cache path.
|
|
"""
|
|
|
|
from collections.abc import Mapping
|
|
|
|
import torch
|
|
from vllm.logger import init_logger
|
|
|
|
logger = init_logger(__name__)
|
|
|
|
# Flat payload keys partitioned at worker output into inter-stage connector
|
|
# payloads vs client-facing multimodal outputs. Only final output roots are
|
|
# listed here; everything else remains available for stage-to-stage transport.
|
|
_CLIENT_MM_ROOT_KEYS: frozenset[str] = frozenset(
|
|
{
|
|
"model_outputs",
|
|
"sr",
|
|
"audio",
|
|
"image",
|
|
"images",
|
|
"video",
|
|
"videos",
|
|
"trajectory_latents",
|
|
"latents",
|
|
}
|
|
)
|
|
|
|
_CLIENT_MM_META_KEYS: frozenset[str] = frozenset(
|
|
{
|
|
"audio_text_total_chars",
|
|
"duplex_epoch",
|
|
"duplex_turn_id",
|
|
"llm_output_text_utf8",
|
|
"segment_end",
|
|
"tts_is_last_chunk",
|
|
"turn_end",
|
|
}
|
|
)
|
|
|
|
|
|
def partition_flat_payload(
|
|
payload: Mapping[str, object],
|
|
) -> tuple[dict[str, object], dict[str, object]]:
|
|
"""Split a flattened per-request payload into inter-stage vs client mm dicts."""
|
|
if not payload:
|
|
return {}, {}
|
|
inter_stage: dict[str, object] = {}
|
|
client_mm: dict[str, object] = {}
|
|
for key, value in payload.items():
|
|
root = key.split(".", 1)[0]
|
|
if root in _CLIENT_MM_ROOT_KEYS:
|
|
client_mm[key] = value
|
|
elif root == "meta" and "." in key and key.split(".", 1)[1] in _CLIENT_MM_META_KEYS:
|
|
# Small final-output metadata needed by serving (for example
|
|
# transcript text attached to audio) must ride with client MM
|
|
# output. Keep it in inter-stage too so downstream stages that read
|
|
# metadata from the full payload are not starved.
|
|
inter_stage[key] = value
|
|
client_mm[key] = value
|
|
else:
|
|
inter_stage[key] = value
|
|
return inter_stage, client_mm
|
|
|
|
|
|
def partition_payload_list(
|
|
payloads: list[dict[str, object]],
|
|
) -> tuple[list[dict[str, object] | None] | None, list[dict[str, object] | None] | None]:
|
|
inter_stage_list: list[dict[str, object] | None] = []
|
|
client_mm_list: list[dict[str, object] | None] = []
|
|
for payload in payloads:
|
|
inter_stage, client_mm = partition_flat_payload(payload)
|
|
inter_stage_list.append(inter_stage or None)
|
|
client_mm_list.append(client_mm or None)
|
|
return (
|
|
None if all(item is None for item in inter_stage_list) else inter_stage_list,
|
|
None if all(item is None for item in client_mm_list) else client_mm_list,
|
|
)
|
|
|
|
|
|
def build_mm_cpu(multimodal_outputs: dict) -> dict[str, object]:
|
|
"""Pre-copies multimodal tensor to CPU once (not per-request) to avoid
|
|
redundant D2H transfers when gpu_resident_buffer_keys keeps them on GPU.
|
|
|
|
In the case of prefix caching, the multimodal outputs provided will
|
|
only contain the passthrough data.
|
|
|
|
Args:
|
|
multimodal_outputs: Multimodal dict mapping strings to objects.
|
|
"""
|
|
if not multimodal_outputs:
|
|
return {}
|
|
|
|
# Pre-copy multimodal tensors to CPU once (not per-request) to avoid
|
|
# redundant D2H transfers when gpu_resident_buffer_keys keeps them on GPU.
|
|
mm_cpu: dict[str, object] = {}
|
|
# Currently there are some cases where this is true at the
|
|
# moment, which should be fixed.
|
|
if not isinstance(multimodal_outputs, Mapping):
|
|
logger.warning("Multimodal outputs are not a dict and will not be passed")
|
|
|
|
for k, v in multimodal_outputs.items():
|
|
cpu_v = _to_cpu(v)
|
|
if cpu_v is not None:
|
|
mm_cpu[k] = cpu_v
|
|
return mm_cpu
|
|
|
|
|
|
def snapshot_mm_payload(multimodal_outputs: dict) -> dict[str, object]:
|
|
"""Snapshot a multimodal payload without moving it off its device.
|
|
|
|
Request-end full-payload producers retain these tensors across decode
|
|
steps. CUDA graph outputs and runner input buffers may be reused on the
|
|
next step, so retaining views is not sufficient: CUDA tensors must be
|
|
copied first. Compatible per-request tensor lists are packed with one
|
|
``torch.cat`` and restored as views, avoiding one tiny copy kernel per
|
|
active request.
|
|
"""
|
|
if not multimodal_outputs:
|
|
return {}
|
|
return {key: _snapshot_payload_value(value) for key, value in multimodal_outputs.items()}
|
|
|
|
|
|
def _snapshot_payload_value(value):
|
|
if isinstance(value, torch.Tensor):
|
|
tensor = value.detach()
|
|
return tensor.clone() if tensor.device.type != "cpu" else tensor
|
|
if isinstance(value, dict):
|
|
return {key: _snapshot_payload_value(item) for key, item in value.items()}
|
|
if isinstance(value, list):
|
|
if not value:
|
|
return value
|
|
first = value[0]
|
|
if (
|
|
len(value) > 1
|
|
and isinstance(first, torch.Tensor)
|
|
and first.ndim > 0
|
|
and first.layout == torch.strided
|
|
and all(
|
|
isinstance(item, torch.Tensor)
|
|
and item.ndim == first.ndim
|
|
and item.layout == first.layout
|
|
and item.device == first.device
|
|
and item.dtype == first.dtype
|
|
and tuple(item.shape[1:]) == tuple(first.shape[1:])
|
|
for item in value
|
|
)
|
|
):
|
|
packed = torch.cat([item.detach() for item in value], dim=0)
|
|
output = []
|
|
offset = 0
|
|
for item in value:
|
|
length = item.shape[0]
|
|
output.append(packed.narrow(0, offset, length))
|
|
offset += length
|
|
return output
|
|
return [_snapshot_payload_value(item) for item in value]
|
|
if isinstance(value, tuple):
|
|
return tuple(_snapshot_payload_value(item) for item in value)
|
|
return value
|
|
|
|
|
|
def _to_cpu(value):
|
|
"""Recursively detach + move tensors to CPU; preserve dict/list nesting."""
|
|
if isinstance(value, torch.Tensor):
|
|
return value.detach().to("cpu").contiguous()
|
|
if isinstance(value, dict):
|
|
out = {}
|
|
for k, v in value.items():
|
|
cpu_v = _to_cpu(v)
|
|
if cpu_v is not None:
|
|
out[k] = cpu_v
|
|
return out or None
|
|
if isinstance(value, list):
|
|
if not value:
|
|
return value
|
|
return [_to_cpu(v) for v in value]
|
|
return value
|
|
|
|
|
|
def to_payload_element(
|
|
element: object,
|
|
idx: int,
|
|
start: int,
|
|
end: int,
|
|
pass_lists_through: bool = False,
|
|
seq_len: int | None = None,
|
|
scheduled_seq_len: int | None = None,
|
|
):
|
|
"""Build an mm payload element corresponding to one request index
|
|
from an element containing 0 or more CPU tensors.
|
|
|
|
Args:
|
|
element: The object to be added to the payload.
|
|
idx: The index of the request.
|
|
start: The start index corresponding to the request idx.
|
|
end: The end index corresponding to the request idx.
|
|
pass_lists_through: bool Whether or not lists should be treated as
|
|
passthrough data; this should be False in normal cases, but True
|
|
if we need to avoid splitting nonempty lists prior to calling
|
|
postprocess, which is the case for prefix cache.
|
|
seq_len: Optional hidden-aligned batch length (``hidden_states.shape[0]``).
|
|
When a tensor's first dimension equals this value, it is sliced
|
|
per request using ``start:end``.
|
|
scheduled_seq_len: Optional scheduler-aligned batch length
|
|
(``scheduler_output.total_num_scheduled_tokens``). Some full-payload
|
|
mm tensors (e.g. batched ``codes.audio`` with tail-only hidden states)
|
|
are laid out by scheduled tokens instead of the hidden tail shape.
|
|
When omitted, ``seq_len`` is reused for backward compatibility.
|
|
"""
|
|
if scheduled_seq_len is None:
|
|
scheduled_seq_len = seq_len
|
|
|
|
# Cached per-token tensors are merged elsewhere; here a first dim equal to
|
|
# either the hidden-aligned or scheduler-aligned batch length means a
|
|
# per-request slice is required.
|
|
if isinstance(element, torch.Tensor) and (
|
|
(seq_len is not None and element.shape[0] == seq_len)
|
|
or (scheduled_seq_len is not None and element.shape[0] == scheduled_seq_len)
|
|
):
|
|
return element[start:end].contiguous()
|
|
# Every other case is shared between prefix cache (passthrough data)
|
|
# and running a model without prefix caching.
|
|
elif isinstance(element, dict):
|
|
return {
|
|
sk: to_payload_element(
|
|
sv,
|
|
idx,
|
|
start,
|
|
end,
|
|
pass_lists_through=pass_lists_through,
|
|
seq_len=seq_len,
|
|
scheduled_seq_len=scheduled_seq_len,
|
|
)
|
|
for sk, sv in element.items()
|
|
}
|
|
elif isinstance(element, list):
|
|
# For lists, clone tensors to avoid cross-request aliasing
|
|
if pass_lists_through:
|
|
return [elem.clone() if isinstance(elem, torch.Tensor) else elem for elem in element]
|
|
element = element[idx] if idx < len(element) else element[0]
|
|
if isinstance(element, torch.Tensor):
|
|
element = element.clone()
|
|
return element
|
|
elif isinstance(element, torch.Tensor):
|
|
# List-derived tensor payloads are request-invariant; clone to
|
|
# avoid accidental cross-request aliasing on downstream mutation.
|
|
return element.clone()
|
|
return element
|