Files

635 lines
25 KiB
Python
Raw Permalink Normal View History

# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""IPC utilities for transferring large tensors via POSIX shared memory.
Used by Hop1 (GPU worker <-> scheduler) to avoid pickling large video tensors
through the MessageQueue. Tensors above ``_SHM_TENSOR_THRESHOLD`` are copied
into a named shared-memory segment; only a lightweight metadata dict is
serialised through the queue.
"""
from __future__ import annotations
from collections.abc import Iterator
from contextlib import suppress
from typing import Any, TypeGuard
import numpy as np
import torch
from vllm.logger import init_logger
from vllm_omni.diffusion.data import DiffusionOutput
from vllm_omni.diffusion.media import (
DiffusionMediaOutput,
FloatVideoConsumer,
VideoColorModel,
VideoMediaOutput,
VideoTensorEncoding,
VideoTensorLayout,
VideoTensorSpec,
VideoTransportConstraints,
VideoValueRange,
)
_SHM_TENSOR_THRESHOLD = 1_000_000 # 1 MB
DIFFUSION_RPC_RESULT_ENVELOPE = "diffusion_rpc_result"
_DIFFUSION_MEDIA_WIRE_TYPE = "diffusion_media_v1"
logger = init_logger(__name__)
# An uncertain CUDA cleanup must not unmap memory still used by the device.
# Retain failed transfers until worker exit and reject further registrations.
_failed_shm_copies: list[tuple[Any, torch.Tensor, torch.Tensor, torch.Stream]] = []
# Sentinel so compute-then-assign packing can tell "field not packed" apart from
# "field packed to None".
_UNSET = object()
class _SharedMemoryLease:
"""Keep an unlinked shared-memory mapping alive while array views use it."""
def __init__(self, shm: Any, array: np.ndarray) -> None:
self.shm = shm
self.__array_interface__ = array.__array_interface__
def __del__(self) -> None:
with suppress(BufferError):
self.shm.close()
def _new_shm_array(shape: tuple[int, ...], dtype: np.dtype) -> tuple[Any, np.ndarray, dict[str, Any]]:
from multiprocessing import shared_memory
nbytes = int(np.prod(shape, dtype=np.int64)) * dtype.itemsize
shm = shared_memory.SharedMemory(create=True, size=nbytes)
array = np.ndarray(shape, dtype=dtype, buffer=shm.buf[:nbytes])
handle = {
"name": shm.name,
"shape": list(shape),
"numpy_dtype": str(dtype),
"nbytes": nbytes,
}
return shm, array, handle
def _array_to_shm(array: np.ndarray) -> dict[str, Any]:
"""Copy a contiguous NumPy-compatible array into shared memory."""
if array.dtype.hasobject:
raise TypeError("NumPy object arrays cannot be transferred through raw shared memory")
array = np.ascontiguousarray(array)
shm, shm_array, handle = _new_shm_array(array.shape, array.dtype)
np.copyto(shm_array, array)
del shm_array
shm.close()
return handle
def _array_from_shm(handle: dict[str, Any]) -> np.ndarray:
"""Materialize or borrow an array from shared memory and unlink its name."""
from multiprocessing import shared_memory
shm = shared_memory.SharedMemory(name=handle["name"])
array = np.ndarray(
handle["shape"],
dtype=np.dtype(handle["numpy_dtype"]),
buffer=shm.buf[: handle["nbytes"]],
)
if handle.get("borrow_on_unpack"):
# A base owner survives np.asarray(), slicing, and torch.from_numpy().
# An ndarray subclass attribute does not survive np.asarray().
borrowed = np.asarray(_SharedMemoryLease(shm, array))
shm.unlink()
return borrowed
try:
return array.copy()
finally:
del array
shm.close()
shm.unlink()
def _cuda_registered_shm_copy(tensor: torch.Tensor, d2h_stream: torch.Stream) -> dict[str, Any]:
"""Copy a CUDA tensor directly into a CUDA-registered POSIX SHM segment."""
if _failed_shm_copies:
raise RuntimeError("A previous registered SHM transfer failed CUDA cleanup; restart the worker")
original_dtype = tensor.dtype
source = tensor.detach()
# NumPy has no BF16 dtype. Transfer the original bits, without allocating
# an FP32 accelerator tensor or converting the borrowed CPU result.
if original_dtype == torch.bfloat16:
numpy_dtype = np.dtype(np.uint16)
else:
numpy_dtype = torch.empty((), dtype=source.dtype).numpy().dtype
cudart = torch.cuda.cudart()
shm, shm_array, handle = _new_shm_array(tuple(source.shape), numpy_dtype)
pointer = shm_array.ctypes.data
host_tensor = None
registered = False
succeeded = False
try:
host_tensor = torch.from_numpy(shm_array).view(original_dtype)
error = cudart.cudaHostRegister(pointer, handle["nbytes"], 0)
if int(error) != 0:
raise RuntimeError(f"cudaHostRegister failed: {cudart.cudaGetErrorString(error)}")
registered = True
old_stream = torch.accelerator.current_stream()
torch.accelerator.set_stream(d2h_stream)
try:
host_tensor.copy_(source, non_blocking=True)
finally:
torch.accelerator.set_stream(old_stream)
d2h_stream.synchronize()
error = cudart.cudaHostUnregister(pointer)
if int(error) != 0:
raise RuntimeError(f"cudaHostUnregister failed: {cudart.cudaGetErrorString(error)}")
registered = False
handle["borrow_on_unpack"] = True
handle.update(
{
"__tensor_shm__": True,
"torch_dtype": str(original_dtype),
"torch_dtype_view": original_dtype == torch.bfloat16,
}
)
succeeded = True
return handle
finally:
if registered:
try:
# A copy may have been enqueued before a later operation failed.
d2h_stream.synchronize()
error = cudart.cudaHostUnregister(pointer)
if int(error) != 0:
raise RuntimeError(f"cudaHostUnregister failed: {cudart.cudaGetErrorString(error)}")
registered = False
except Exception:
assert host_tensor is not None
_failed_shm_copies.append((shm, host_tensor, source, d2h_stream))
logger.exception("Retaining registered SHM after CUDA cleanup failure; worker restart required")
del host_tensor, shm_array
if not registered:
shm.close()
if not succeeded:
shm.unlink()
def _unlink_shm_handle(handle: dict[str, Any]) -> None:
"""Best-effort removal of a shared-memory segment created during packing."""
from multiprocessing import shared_memory
name = handle.get("name")
if not name:
return
try:
shm = shared_memory.SharedMemory(name=name)
except FileNotFoundError:
return
try:
shm.close()
finally:
try:
shm.unlink()
except FileNotFoundError:
pass
def _unlink_shm_handles(handles: list[dict[str, Any]]) -> None:
for handle in handles:
try:
_unlink_shm_handle(handle)
except Exception:
# Cleanup is best-effort; never mask the original packing failure.
pass
def _tensor_to_shm(
tensor: torch.Tensor,
d2h_stream: torch.Stream | None = None,
created: list[dict[str, Any]] | None = None,
*,
enable_registered_shm: bool = False,
) -> dict[str, Any]:
"""Copy a tensor into POSIX shared memory and return a metadata handle.
The shared memory segment remains alive after this call (the local fd is
closed, but the segment persists until ``_tensor_from_shm`` unlinks it).
If *d2h_stream* is provided, the D2H copy uses ``copy_()`` with
``pin_memory=True`` on that stream instead of the synchronous ``.cpu()``
path. The caller must synchronize *d2h_stream* after all tensors are
packed.
"""
original_dtype = tensor.dtype
if (
d2h_stream is not None
and enable_registered_shm
and tensor.device.type == "cuda"
# ROCm also uses the "cuda" device type, but not this CUDA runtime API.
and torch.version.cuda is not None
):
handle = _cuda_registered_shm_copy(tensor, d2h_stream)
if created is not None:
created.append(handle)
return handle
if d2h_stream is not None:
# Non-blocking D2H: copy on side stream to pinned CPU memory.
old_stream = torch.accelerator.current_stream()
torch.accelerator.set_stream(d2h_stream)
try:
t = tensor.detach()
cpu = torch.empty(t.shape, dtype=t.dtype, pin_memory=True)
cpu.copy_(t, non_blocking=True)
finally:
torch.accelerator.set_stream(old_stream)
d2h_stream.synchronize()
tensor = cpu
# numpy has no bfloat16, so widen on the host after D2H. Never widen on
# the accelerator before the copy: a payload that already fell back after
# an fp32 OOM would OOM again on a second device-side fp32 allocation.
if original_dtype == torch.bfloat16:
tensor = tensor.to(torch.float32)
else:
tensor = tensor.detach().cpu().contiguous()
if original_dtype == torch.bfloat16:
tensor = tensor.to(torch.float32)
handle = _array_to_shm(tensor.numpy())
handle.update(
{
"__tensor_shm__": True,
"torch_dtype": str(original_dtype),
}
)
if created is not None:
created.append(handle)
return handle
def _tensor_from_shm(handle: dict[str, Any]) -> torch.Tensor:
"""Reconstruct a tensor from a shared-memory handle and free the segment."""
tensor = torch.from_numpy(_array_from_shm(handle))
# Restore the original dtype if it differs from the numpy-compatible
# dtype used for the SHM transfer (e.g. bfloat16 → float32 → bfloat16).
torch_dtype_str = handle.get("torch_dtype", "")
if torch_dtype_str:
original_dtype = getattr(torch, torch_dtype_str.replace("torch.", ""), None)
if original_dtype is not None and tensor.dtype != original_dtype:
tensor = tensor.view(original_dtype) if handle.get("torch_dtype_view") else tensor.to(original_dtype)
return tensor
def _pack_tensor_if_large(
val: torch.Tensor,
d2h_stream: torch.Stream | None = None,
created: list[dict[str, Any]] | None = None,
*,
enable_registered_shm: bool = False,
) -> torch.Tensor | dict:
"""Replace a tensor with an SHM handle if it exceeds the threshold.
Batch outputs are split into per-request views that share one storage.
Pickle serializes a view's whole storage, so a batch of small views costs
the full batch tensor once per request. Size the decision on the storage
to keep those views off the wire; packing copies just the view.
"""
view_bytes = val.nelement() * val.element_size()
try:
storage_bytes = val.untyped_storage().nbytes()
except Exception:
storage_bytes = view_bytes
if max(view_bytes, storage_bytes) > _SHM_TENSOR_THRESHOLD:
return _tensor_to_shm(val, d2h_stream=d2h_stream, created=created, enable_registered_shm=enable_registered_shm)
return val
def _ndarray_to_shm(array: np.ndarray, created: list[dict[str, Any]] | None = None) -> dict[str, Any]:
"""Copy a contiguous NumPy array into POSIX shared memory."""
handle = _array_to_shm(array)
handle["__ndarray_shm__"] = True
if created is not None:
created.append(handle)
return handle
def _ndarray_from_shm(handle: dict[str, Any]) -> np.ndarray:
"""Reconstruct a NumPy array from shared memory and free the segment."""
return _array_from_shm(handle)
def _pack_value_if_large(
val: object,
d2h_stream: torch.Stream | None = None,
created: list[dict[str, Any]] | None = None,
) -> object:
2026-06-01 22:04:31 +02:00
"""Recursively replace large tensors with SHM handles.
Walks the container shapes pipelines return as ``DiffusionOutput.output``:
bare tensors, dicts (e.g. Cosmos3 ``{"image"/"video": ...}``), and
tuples/lists (e.g. LTX2 ``(video, audio)``). Other values pass
2026-06-01 22:04:31 +02:00
through unchanged. ``_unpack_if_shm_handle`` must mirror these shapes — keep
the two in sync.
"""
if isinstance(val, torch.Tensor):
return _pack_tensor_if_large(val, d2h_stream=d2h_stream, created=created)
if isinstance(val, np.ndarray):
return (
_ndarray_to_shm(val, created=created)
if not val.dtype.hasobject and val.nbytes > _SHM_TENSOR_THRESHOLD
else val
)
2026-06-01 22:04:31 +02:00
if isinstance(val, dict):
return {key: _pack_value_if_large(value, d2h_stream=d2h_stream, created=created) for key, value in val.items()}
2026-06-01 22:04:31 +02:00
if isinstance(val, list):
return [_pack_value_if_large(item, d2h_stream=d2h_stream, created=created) for item in val]
2026-06-01 22:04:31 +02:00
if isinstance(val, tuple):
return tuple(_pack_value_if_large(item, d2h_stream=d2h_stream, created=created) for item in val)
2026-06-01 22:04:31 +02:00
return val
def _unpack_if_shm_handle(val: object) -> object:
2026-06-01 22:04:31 +02:00
"""Reconstruct tensors from SHM handles, mirroring ``_pack_value_if_large``."""
if isinstance(val, dict) and val.get("__tensor_shm__"):
return _tensor_from_shm(val)
if isinstance(val, dict) and val.get("__ndarray_shm__"):
return _ndarray_from_shm(val)
2026-06-01 22:04:31 +02:00
if isinstance(val, dict):
return {key: _unpack_if_shm_handle(value) for key, value in val.items()}
if isinstance(val, list):
return [_unpack_if_shm_handle(item) for item in val]
if isinstance(val, tuple):
return tuple(_unpack_if_shm_handle(item) for item in val)
return val
def _pack_diffusion_media(
media: DiffusionMediaOutput,
*,
d2h_stream: torch.Stream | None,
created: list[dict[str, Any]] | None = None,
enable_registered_shm: bool = False,
) -> dict[str, Any]:
media.validate()
if not media.prepared_for_transport:
raise ValueError("Diffusion media must be prepared before IPC packing")
video = media.video
packed_tensor = _pack_tensor_if_large(
video.tensor, d2h_stream=d2h_stream, created=created, enable_registered_shm=enable_registered_shm
)
if isinstance(packed_tensor, torch.Tensor) and packed_tensor.device.type != "cpu":
packed_tensor = packed_tensor.detach().cpu().contiguous()
return {
"__type__": _DIFFUSION_MEDIA_WIRE_TYPE,
"schema_version": video.schema_version,
"prepared_for_transport": True,
"video": {
"tensor": packed_tensor,
"layout": video.spec.layout.value,
"encoding": video.spec.encoding.value,
"value_range": video.spec.value_range.value,
"color_model": video.spec.color_model.value,
"pending_float_consumers": sorted(consumer.value for consumer in video.constraints.pending_float_consumers),
},
}
def _unpack_diffusion_media(packed: dict[str, Any]) -> DiffusionMediaOutput:
if packed.get("__type__") != _DIFFUSION_MEDIA_WIRE_TYPE:
raise ValueError("Invalid diffusion media wire payload")
video_payload = packed.get("video")
if not isinstance(video_payload, dict):
raise ValueError("Diffusion media wire payload is missing video metadata")
tensor = _unpack_if_shm_handle(video_payload.get("tensor"))
if not isinstance(tensor, torch.Tensor):
raise ValueError("Diffusion media wire payload contains an invalid video tensor")
schema_version = packed.get("schema_version")
if schema_version != 1:
raise ValueError(f"Unsupported video media schema version: {schema_version}")
if packed.get("prepared_for_transport") is not True:
raise ValueError("Diffusion media wire payload was not prepared for transport")
media = DiffusionMediaOutput(
video=VideoMediaOutput(
tensor=tensor,
spec=VideoTensorSpec(
layout=VideoTensorLayout(video_payload["layout"]),
encoding=VideoTensorEncoding(video_payload["encoding"]),
value_range=VideoValueRange(video_payload["value_range"]),
color_model=VideoColorModel(video_payload["color_model"]),
),
constraints=VideoTransportConstraints(
pending_float_consumers=frozenset(
FloatVideoConsumer(value) for value in video_payload.get("pending_float_consumers", [])
)
),
schema_version=1,
),
prepared_for_transport=True,
)
media.validate()
return media
def _pack_diffusion_fields(
output: DiffusionOutput,
d2h_stream: torch.Stream | None = None,
created: list[dict[str, Any]] | None = None,
pending_updates: list[tuple[DiffusionOutput, str, object]] | None = None,
*,
enable_registered_shm: bool = False,
) -> DiffusionOutput:
if output.media is not None and output.output is not None:
raise ValueError("DiffusionOutput cannot contain both media and legacy output")
if output.media is not None and not isinstance(output.media, DiffusionMediaOutput):
raise TypeError(f"DiffusionOutput.media must be DiffusionMediaOutput, got {type(output.media).__name__}")
# Pack into locals first so a mid-payload failure leaves ``output`` unmutated
# (never enqueue a half-packed object); the caller unlinks any SHM segments
# accumulated in ``created`` on failure.
packed_output = _UNSET
if enable_registered_shm and output.video_output_index is not None:
index = output.video_output_index
values = output.output
if not isinstance(values, tuple) or type(index) is not int or not 0 <= index < len(values):
raise ValueError("video_output_index must identify a video tensor in the legacy output tuple")
if not isinstance(values[index], torch.Tensor):
raise TypeError("The legacy video output must be a tensor")
packed_output = tuple(
_pack_tensor_if_large(value, d2h_stream=d2h_stream, created=created, enable_registered_shm=True)
if position == index
else _pack_value_if_large(value, d2h_stream=d2h_stream, created=created)
for position, value in enumerate(values)
)
elif output.output is not None:
packed_output = _pack_value_if_large(output.output, d2h_stream=d2h_stream, created=created)
packed_media = _UNSET
if output.media is not None:
packed_media = _pack_diffusion_media(
output.media, d2h_stream=d2h_stream, created=created, enable_registered_shm=enable_registered_shm
)
packed_latents = _UNSET
if isinstance(output.trajectory_latents, torch.Tensor):
packed_latents = _pack_tensor_if_large(output.trajectory_latents, d2h_stream=d2h_stream, created=created)
packed_timesteps = _UNSET
if isinstance(output.trajectory_timesteps, torch.Tensor):
packed_timesteps = _pack_tensor_if_large(output.trajectory_timesteps, d2h_stream=d2h_stream, created=created)
packed_log_probs = _UNSET
if isinstance(output.trajectory_log_probs, torch.Tensor):
packed_log_probs = _pack_tensor_if_large(output.trajectory_log_probs, d2h_stream=d2h_stream, created=created)
updates = [
(output, field_name, value)
for field_name, value in (
("output", packed_output),
("media", packed_media),
("trajectory_latents", packed_latents),
("trajectory_timesteps", packed_timesteps),
("trajectory_log_probs", packed_log_probs),
)
if value is not _UNSET
]
if pending_updates is None:
for target, field_name, value in updates:
setattr(target, field_name, value)
else:
pending_updates.extend(updates)
return output
def _is_rpc_result_envelope(output: object) -> TypeGuard[dict[str, Any]]:
return isinstance(output, dict) and output.get("type") == DIFFUSION_RPC_RESULT_ENVELOPE
def pack_diffusion_output_shm(
output: object,
d2h_stream: torch.Stream | None = None,
*,
enable_registered_shm: bool = False,
) -> object:
"""Replace large tensors in diffusion worker outputs with SHM handles.
Supports a bare ``DiffusionOutput``, a wrapper object carrying one in
``.result`` (for example ``RunnerOutput``), an RPC result envelope carrying
the diffusion output in ``["result"]``, a batch wrapper carrying
``RunnerOutput`` objects in ``.runner_outputs``, or a DP-tagged dict
``{"dp_rank": int, "output": DiffusionOutput}`` used by DP multi-concurrency.
If *d2h_stream* is provided, D2H copies use that stream (non-blocking on
the default stream). The caller must synchronize *d2h_stream* afterward.
``enable_registered_shm`` opts explicitly identified video tensors into
direct CUDA-to-SHM transfer and borrowed CPU views. Other payloads keep
their existing transport. Registration is per transfer, without pooling.
Packing is failure-atomic: every SHM segment created during the call is
tracked and unlinked if any part of the payload fails to pack, and field
mutations are committed only after the complete payload succeeds.
"""
created: list[dict[str, Any]] = []
pending_updates: list[tuple[DiffusionOutput, str, object]] = []
try:
packed = _pack_output_shm(
output,
d2h_stream=d2h_stream,
created=created,
pending_updates=pending_updates,
enable_registered_shm=enable_registered_shm,
)
except BaseException:
_unlink_shm_handles(created)
raise
# Commit mutations only after every field and batch entry packed. Until
# this point, a failure leaves the complete caller-owned payload untouched.
for target, field_name, value in pending_updates:
setattr(target, field_name, value)
return packed
def _iter_diffusion_outputs(output: object) -> Iterator[DiffusionOutput]:
"""Yield every packable ``DiffusionOutput`` leaf in a worker payload."""
if isinstance(output, DiffusionOutput):
yield output
return
# DP multi-concurrency: {"dp_rank": int, "output": DiffusionOutput}
if isinstance(output, dict) and "dp_rank" in output and "output" in output:
inner = output["output"]
if isinstance(inner, DiffusionOutput):
yield inner
return
if _is_rpc_result_envelope(output):
yield from _iter_diffusion_outputs(output.get("result"))
return
result = getattr(output, "result", None)
if isinstance(result, DiffusionOutput):
yield result
runner_outputs = getattr(output, "runner_outputs", None)
if isinstance(runner_outputs, list):
for runner_output in runner_outputs:
yield from _iter_diffusion_outputs(runner_output)
def payload_carries_typed_media(output: object) -> bool:
"""True if *output* carries any typed ``DiffusionOutput.media`` payload."""
return any(item.media is not None for item in _iter_diffusion_outputs(output))
def _pack_output_shm(
output: object,
*,
d2h_stream: torch.Stream | None,
created: list[dict[str, Any]],
pending_updates: list[tuple[DiffusionOutput, str, object]],
enable_registered_shm: bool = False,
) -> object:
transport_options = {"enable_registered_shm": True} if enable_registered_shm else {}
for item in _iter_diffusion_outputs(output):
_pack_diffusion_fields(
item,
d2h_stream=d2h_stream,
created=created,
pending_updates=pending_updates,
**transport_options,
)
return output
def _unpack_diffusion_fields(output: DiffusionOutput) -> DiffusionOutput:
output.output = _unpack_if_shm_handle(output.output)
if isinstance(output.media, dict):
output.media = _unpack_diffusion_media(output.media)
output.trajectory_latents = _unpack_if_shm_handle(output.trajectory_latents)
output.trajectory_timesteps = _unpack_if_shm_handle(output.trajectory_timesteps)
output.trajectory_log_probs = _unpack_if_shm_handle(output.trajectory_log_probs)
return output
def unpack_diffusion_output_shm(output: object) -> object:
"""Reconstruct tensors from SHM handles in diffusion worker outputs."""
if isinstance(output, DiffusionOutput):
return _unpack_diffusion_fields(output)
# DP multi-concurrency: {"dp_rank": int, "output": DiffusionOutput}
if isinstance(output, dict) and "dp_rank" in output and "output" in output:
inner = output["output"]
if isinstance(inner, DiffusionOutput):
output["output"] = _unpack_diffusion_fields(inner)
return output
if _is_rpc_result_envelope(output):
result = output.get("result")
output["result"] = unpack_diffusion_output_shm(result)
return output
result = getattr(output, "result", None)
if isinstance(result, DiffusionOutput):
setattr(output, "result", _unpack_diffusion_fields(result))
runner_outputs = getattr(output, "runner_outputs", None)
if isinstance(runner_outputs, list):
for runner_output in runner_outputs:
unpack_diffusion_output_shm(runner_output)
return output