Files
wehub-resource-sync 59a0a3844c
PR Test AMD / cancel-on-close (push) Has been skipped
PR Test NVIDIA ARM / scan (push) Has been skipped
PR Test NVIDIA / cancel-on-close (push) Has been skipped
PR Test AMD / scan (push) Has been skipped
PR Test NVIDIA ARM / cancel-on-close (push) Has been skipped
PR Test NVIDIA / scan (push) Has been skipped
Release Docker Images / build (cu129-torch-2.11.0) (push) Has been skipped
Release Docker Images / build (cu130-torch-2.11.0) (push) Has been skipped
Release PyPI / publish (push) Has been skipped
Scheduler Python Test / test (push) Successful in 27m19s
Docs / build (push) Successful in 28m8s
Scheduler C++ Test / test (push) Successful in 28m19s
Scheduler C++ Test / test-flat (push) Successful in 28m18s
Docs / deploy (push) Has been cancelled
PR Test AMD / finish (push) Has been cancelled
PR Test NVIDIA / finish (push) Has been cancelled
PR Test NVIDIA ARM / finish (push) Has been cancelled
PR Test NVIDIA ARM / ${{ matrix.name }} (${{ matrix.runner }}) (push) Has been cancelled
PR Test AMD / ${{ matrix.name }} (${{ matrix.runner }}) (push) Has been cancelled
PR Test NVIDIA / ${{ matrix.name }} (${{ matrix.runner }}) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:32:31 +08:00

192 lines
7.9 KiB
Python

# Copyright (c) 2026 LightSeek Foundation
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
"""Multimodal request data structures used across processors and model adapters."""
from __future__ import annotations
import dataclasses
import uuid
from enum import Enum, auto
from typing import Any
import numpy as np
import torch
from tokenspeed.runtime.multimodal.hash import hash_feature
from tokenspeed.runtime.multimodal.shm_transport import ShmTensorHandle
from tokenspeed.runtime.utils.env import envs
# Multimodal pad-value substitute IDs: a placeholder mm token's id is rewritten
# to ``_MM_PAD_BASE + (hash & _MM_PAD_HASH_MASK)`` so duplicate features share
# the same substitute and prefix-match in the text-only prefix cache. The base
# sits well above any text vocab; the 30-bit mask keeps cross-hash collisions
# rare enough for long-running servers (~10^9 slots).
_MM_PAD_BASE = 1_000_000
_MM_PAD_HASH_MASK = (1 << 30) - 1
def is_mm_pad_value(token_ids: torch.Tensor) -> torch.Tensor:
"""Bool mask of positions rewritten to a hash-derived multimodal pad id."""
return (token_ids >= _MM_PAD_BASE) & (token_ids <= _MM_PAD_BASE + _MM_PAD_HASH_MASK)
def maybe_substitute_mm_pad(
input_ids: torch.Tensor, substitute_id: int | None
) -> torch.Tensor:
"""Replace hash mm-pad positions with ``substitute_id``; no-op if None."""
if substitute_id is None:
return input_ids
return input_ids.masked_fill(is_mm_pad_value(input_ids), substitute_id)
class Modality(Enum):
IMAGE = auto()
VIDEO = auto()
AUDIO = auto()
# ``eq=False`` on every dataclass below: tensor-valued fields crash the
# default element-wise ``__eq__`` and force ``__hash__`` to None.
@dataclasses.dataclass(eq=False)
class MultimodalDataItem:
modality: Modality
hash: int | None = None
pad_value: int | None = None
offsets: list | None = None
feature: torch.Tensor | np.ndarray | ShmTensorHandle | None = None
model_specific_data: dict[str, Any] = dataclasses.field(default_factory=dict)
# Encoder output for this item, populated on first encoder pass and reused
# across chunked-prefill iterations of the owning request. Lifetime is
# tied to the request: when the request finishes the item is GC'd and
# these tensors are released. ``encoded_deepstack`` is set only for
# deepstack-enabled modalities.
encoded: torch.Tensor | None = None
encoded_deepstack: torch.Tensor | None = None
# EPD (encode-prefill-decode): when set, this item's embedding is received
# from an encode worker over Mooncake into ``encoded`` instead of running the
# vision tower. A dict ``{bootstrap_room, bootstrap_host, bootstrap_port}``
# naming the encode worker's rendezvous for this item's image (one room per
# item: the gateway splits the mm payload one item per image and the encode
# worker row-splits the concatenated-subgrid embedding per item). None for
# non-EPD items (left to the vision tower).
encode_handshake: dict | None = None
def __getattr__(self, name: str):
if (
"model_specific_data" in self.__dict__
and name in self.__dict__["model_specific_data"]
):
return self.__dict__["model_specific_data"][name]
raise AttributeError(f"'{self.__class__.__name__}' has no attribute '{name}'")
def ensure_hash(self):
"""Resolve ``self.hash`` to a concrete content id, lazily.
The hash is resolved on demand rather than at construction because it
is usually supplied by the caller, a SHM-backed feature cannot be
hashed here without reading shared memory, and hashing inline bytes is
only worth doing once the value is actually needed.
Resolution order:
* ``TOKENSPEED_MM_SKIP_COMPUTE_HASH`` -> a random id (dedup disabled);
* an already-set hash (e.g. the gateway-provided ``content_hash`` for
image/video) is kept as-is, no recompute;
* inline features the gateway does not hash (e.g. audio) are hashed
in-engine via ``hash_feature``;
* SHM-backed features must carry a caller-provided hash, else raise --
we cannot hash a handle without reading shared memory.
"""
if envs.TOKENSPEED_MM_SKIP_COMPUTE_HASH.get():
self.hash = uuid.uuid4().int
elif self.hash is None:
if isinstance(self.feature, ShmTensorHandle):
raise ValueError(
"SHM-backed multimodal items must carry content hash or "
"pad_value before TokenSpeed consumes them"
)
self.hash = hash_feature(self.feature)
if self.hash is None:
raise RuntimeError("Failed to resolve multimodal item hash.")
def set_pad_value(self):
if self.pad_value is not None:
return
self.ensure_hash()
self.pad_value = _MM_PAD_BASE + (self.hash & _MM_PAD_HASH_MASK)
def is_modality(self, modality: Modality) -> bool:
return self.modality == modality
@dataclasses.dataclass(eq=False)
class MultimodalInputs:
mm_items: list[MultimodalDataItem]
im_token_id: int | None = None
video_token_id: int | None = None
mrope_positions: torch.Tensor | None = None
mrope_position_delta: torch.Tensor | None = None
mrope_position_delta_scalar: int | None = None
mrope_position_delta_repeated_cache: torch.Tensor | None = None
def ensure_pad_values(self) -> None:
for item in self.mm_items:
item.set_pad_value()
def publish_shm_features(self) -> None:
for item in self.mm_items:
if isinstance(item.feature, torch.Tensor):
item.feature = ShmTensorHandle.publish(item.feature)
def attach_shm_features(self) -> None:
"""Open every pending handle on this rank. Must run before the
cross-rank barrier in ``request_handler.recv_reqs``.
"""
for item in self.mm_items:
if isinstance(item.feature, ShmTensorHandle):
item.feature.attach()
def release_shm_features(self) -> None:
for item in self.mm_items:
if isinstance(item.feature, ShmTensorHandle):
item.feature.release()
item.feature = None
def has_pending_shm_features(self) -> bool:
return any(isinstance(item.feature, ShmTensorHandle) for item in self.mm_items)
@dataclasses.dataclass(eq=False)
class MultimodalForwardContext:
"""Per-forward multimodal metadata for prefill embedding replacement."""
mm_inputs: list[MultimodalInputs | None]
extend_prefix_lens: list[int]
extend_seq_lens: list[int]
def has_inputs(self) -> bool:
return bool(self.mm_inputs and any(x is not None for x in self.mm_inputs))
def has_extend_inputs(self) -> bool:
return any(
mm_input is not None and index < len(self.extend_seq_lens)
for index, mm_input in enumerate(self.mm_inputs)
)