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

548 lines
21 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.
"""Assemble LM input embeddings with multimodal encoder tokens spliced in.
Three sequential phases:
1. ``_plan`` walks the active multimodal inputs in the current forward
batch and emits an :class:`EncodePlan` listing (a) the unique items
that still need to be encoded this iteration and (b) every flat
position in ``input_ids`` that should be filled from an encoder token,
along with the source range inside the owning item's encoded tensor.
2. ``_encode`` invokes the model-supplied encoder once per modality with
every miss in the batch in a single call, then writes each item's
output back onto the item itself (``item.encoded`` /
``item.encoded_deepstack``).
3. ``_assemble`` runs the text-token embedding lookup and slices the
encoder-token ranges into the right positions using the plan's
:class:`ScatterRange` records.
Per-item encoded tensors live on the :class:`MultimodalDataItem` itself,
not in an engine-global cache. Lifetime tracks the owning request: when
the request finishes and its ``RequestState`` is dropped, the tensors are
released by GC. Across chunked-prefill iterations of the same request the
item is identical Python object, so the second chunk sees ``item.encoded``
already set and skips re-encoding.
Within a single forward batch we still de-duplicate by modality and
``item.hash``: if two requests reference the same media content using
the same modality, only the first item is fed to the encoder; the second
request's scatter ranges read from the first item's ``encoded`` tensor.
"""
from __future__ import annotations
import logging
import time
from collections import defaultdict
from collections.abc import Callable
from dataclasses import dataclass, field
from typing import Any
import torch
from torch import nn
from tokenspeed.runtime.multimodal.inputs import (
Modality,
MultimodalDataItem,
MultimodalForwardContext,
MultimodalInputs,
)
from tokenspeed.runtime.multimodal.shm_transport import ShmTensorHandle
from tokenspeed.runtime.utils.env import envs
EncoderFn = Callable[[list[MultimodalDataItem]], torch.Tensor]
logger = logging.getLogger(__name__)
LOG_MM_TIMING = envs.TOKENSPEED_LOG_MM_TIMING.get()
@dataclass
class EncoderSpec:
"""Per-modality encoder registration.
Bundles the encoder callable with whether its output needs to be
split into a main + deepstack pair via the model's
``separate_deepstack_embeds`` hook.
"""
fn: EncoderFn
deepstack: bool = False
# ---------------------------------------------------------------------------
# Input-id padding helper
# ---------------------------------------------------------------------------
def pad_input_tokens(input_ids: list[int], mm_inputs: MultimodalInputs) -> list[int]:
"""Substitute placeholder token IDs with each item's ``pad_value``.
The gateway produces ``input_ids`` with a single placeholder token
repeated across every multimodal-token position (e.g. ``<image>``
repeated 1024 times for a 1024-token image). The prefix cache needs
each placeholder run to carry a content-derived ID so two different
images compare unequal. We rewrite each ``offsets`` range to the
item's pre-computed ``pad_value`` here.
"""
if not input_ids or not mm_inputs.mm_items:
return input_ids
out = None
for item in mm_inputs.mm_items:
if item.pad_value is None or not item.offsets:
continue
if out is None:
out = list(input_ids)
pad_value = int(item.pad_value)
for offset_start, offset_end in item.offsets:
out[offset_start : offset_end + 1] = [pad_value] * (
offset_end - offset_start + 1
)
return input_ids if out is None else out
# ---------------------------------------------------------------------------
# Plan structures
# ---------------------------------------------------------------------------
@dataclass(frozen=True)
class ScatterRange:
"""One contiguous range to fill with multimodal encoder tokens.
``flat_dst_*`` are positions in the batch-flat ``input_ids`` tensor
(inclusive on both ends). ``item_src_*`` are positions within
``item.encoded`` (also inclusive). ``item`` is the *canonical* item
holding the encoded tensor — for within-batch dedup'd entries it may
differ from the request-local item that produced the offsets.
"""
flat_dst_start: int
flat_dst_end: int
item: MultimodalDataItem
item_src_start: int
item_src_end: int
@dataclass
class EncodePlan:
"""Work to do this prefill iteration.
``misses_by_modality`` lists the canonical items the encoder needs to
process; each modality/content-hash pair appears at most once.
``scatter_ranges`` describes every place an encoder token must land.
"""
misses_by_modality: dict[Modality, list[MultimodalDataItem]] = field(
default_factory=lambda: defaultdict(list)
)
scatter_ranges: list[ScatterRange] = field(default_factory=list)
aliases_by_canonical: dict[MultimodalDataItem, list[MultimodalDataItem]] = field(
default_factory=lambda: defaultdict(list)
)
def __bool__(self) -> bool:
return bool(self.scatter_ranges)
def _item_token_count(item: MultimodalDataItem) -> int:
"""Total encoded tokens for an item. One offset per subgrid; the
encoder concatenates subgrid tokens in offsets order."""
if not item.offsets:
return 0
return sum(end - start + 1 for start, end in item.offsets)
# ---------------------------------------------------------------------------
# MultimodalEmbedder
# ---------------------------------------------------------------------------
class MultimodalEmbedder:
"""Multimodal input embedding pipeline for one model executor."""
def __init__(self) -> None:
self._h2d_stream: torch.cuda.Stream | None = None
# --- public entry point ------------------------------------------------
def apply(
self,
input_ids: torch.Tensor,
text_embedding: nn.Embedding,
ctx: MultimodalForwardContext | None,
encoders: dict[Modality, EncoderSpec],
multimodal_model: nn.Module,
is_decode_or_idle: bool = False,
) -> tuple[torch.Tensor | None, dict[str, Any]]:
"""Compose LM input embeddings with encoder tokens scattered in.
Returns ``(None, {})`` when there is nothing multimodal to do this
forward (decode iteration, or no active multimodal inputs). The
caller falls back to the regular text-only path on that signal.
"""
if is_decode_or_idle or ctx is None or not ctx.has_extend_inputs():
return None, {}
total_started = time.perf_counter() if LOG_MM_TIMING else None
plan_started = time.perf_counter() if LOG_MM_TIMING else None
plan = self._plan(ctx)
plan_elapsed_ms = (
(time.perf_counter() - plan_started) * 1000
if plan_started is not None
else None
)
if not plan:
return None, {}
encode_started = time.perf_counter() if LOG_MM_TIMING else None
self._encode(plan, encoders, multimodal_model, input_ids.device)
encode_elapsed_ms = (
(time.perf_counter() - encode_started) * 1000
if encode_started is not None
else None
)
alias_started = time.perf_counter() if LOG_MM_TIMING else None
released_alias_features = self._share_encoded_aliases(plan)
alias_elapsed_ms = (
(time.perf_counter() - alias_started) * 1000
if alias_started is not None
else None
)
assemble_started = time.perf_counter() if LOG_MM_TIMING else None
input_embeds, kwargs = self._assemble(
input_ids, text_embedding, plan, encoders, multimodal_model
)
assemble_elapsed_ms = (
(time.perf_counter() - assemble_started) * 1000
if assemble_started is not None
else None
)
cleanup_started = time.perf_counter() if LOG_MM_TIMING else None
released_encoded_features = self._drop_encoded_features(ctx)
cleanup_elapsed_ms = (
(time.perf_counter() - cleanup_started) * 1000
if cleanup_started is not None
else None
)
if LOG_MM_TIMING and total_started is not None:
misses = {
modality.name: len(items)
for modality, items in plan.misses_by_modality.items()
if items
}
logger.info(
"mm_timing multimodal_embedder_apply_ms total=%.3f plan=%.3f "
"encode=%.3f alias=%.3f assemble=%.3f feature_cleanup=%.3f "
"scatter_ranges=%d misses=%s input_rows=%d aliases=%d "
"released_alias_features=%d released_encoded_features=%d",
(time.perf_counter() - total_started) * 1000,
plan_elapsed_ms,
encode_elapsed_ms,
alias_elapsed_ms,
assemble_elapsed_ms,
cleanup_elapsed_ms,
len(plan.scatter_ranges),
misses,
int(input_ids.numel()),
sum(len(items) for items in plan.aliases_by_canonical.values()),
released_alias_features,
released_encoded_features,
)
return input_embeds, kwargs
# --- phase 1: plan -----------------------------------------------------
def _plan(self, ctx: MultimodalForwardContext) -> EncodePlan:
plan = EncodePlan()
if not ctx.mm_inputs:
return plan
# Within-batch dedup: first item per modality and content hash is
# canonical; duplicates reuse its encoded tensor.
canonical_by_key: dict[tuple[Modality, int], MultimodalDataItem] = {}
scheduled: set[MultimodalDataItem] = set()
# Walk the FULL batch (including text-only / decode requests)
# so base offsets line up with the flat input_ids tensor that
# the caller hands us. Requests without mm input contribute
# nothing but still advance ``base``.
base = 0
for req_idx, mm_inputs in enumerate(ctx.mm_inputs):
if req_idx >= len(ctx.extend_seq_lens) or req_idx >= len(
ctx.extend_prefix_lens
):
break
seq = ctx.extend_seq_lens[req_idx]
if mm_inputs is None or seq <= 0:
base += max(seq, 0)
continue
prefix = ctx.extend_prefix_lens[req_idx]
chunk_start = prefix
chunk_end_inc = prefix + seq - 1
for item in mm_inputs.mm_items:
if item is None or not item.offsets:
continue
if item.encoded is not None:
canonical = item
elif (
item.hash is not None
and (item.modality, item.hash) in canonical_by_key
):
canonical = canonical_by_key[(item.modality, item.hash)]
else:
canonical = item
if item.hash is not None:
canonical_by_key[(item.modality, item.hash)] = item
if canonical is not item:
plan.aliases_by_canonical[canonical].append(item)
# src_cursor: start of current subgrid inside item.encoded.
src_cursor = 0
for offset_start, offset_end in item.offsets:
span = offset_end - offset_start + 1
overlap_start = max(offset_start, chunk_start)
overlap_end = min(offset_end, chunk_end_inc)
if overlap_start > overlap_end:
src_cursor += span
continue
plan.scatter_ranges.append(
ScatterRange(
flat_dst_start=base + (overlap_start - prefix),
flat_dst_end=base + (overlap_end - prefix),
item=canonical,
item_src_start=src_cursor + (overlap_start - offset_start),
item_src_end=src_cursor + (overlap_end - offset_start),
)
)
if canonical.encoded is None and canonical not in scheduled:
scheduled.add(canonical)
plan.misses_by_modality[canonical.modality].append(canonical)
src_cursor += span
base += seq
return plan
# --- phase 2: encode ---------------------------------------------------
def _encode(
self,
plan: EncodePlan,
encoders: dict[Modality, EncoderSpec],
multimodal_model: nn.Module,
device: torch.device,
) -> None:
for modality, items in plan.misses_by_modality.items():
if not items:
continue
spec = encoders.get(modality)
if spec is None:
raise RuntimeError(
f"MultimodalEmbedder: no encoder registered for {modality}"
)
move_started = time.perf_counter() if LOG_MM_TIMING else None
self._move_features_to_device(items, device)
move_elapsed_ms = (
(time.perf_counter() - move_started) * 1000
if move_started is not None
else None
)
encoder_started = time.perf_counter() if LOG_MM_TIMING else None
output = spec.fn(items)
if LOG_MM_TIMING and device.type == "cuda":
torch.cuda.synchronize(device)
encoder_elapsed_ms = (
(time.perf_counter() - encoder_started) * 1000
if encoder_started is not None
else None
)
output = output.reshape(-1, output.shape[-1])
per_item_lens = [_item_token_count(it) for it in items]
per_item_embs = torch.split(output, per_item_lens, dim=0)
if spec.deepstack:
for item, emb in zip(items, per_item_embs):
main, deep = multimodal_model.separate_deepstack_embeds(emb)
item.encoded = main
item.encoded_deepstack = deep
else:
for item, emb in zip(items, per_item_embs):
item.encoded = emb
if LOG_MM_TIMING:
logger.info(
"mm_timing encoder_ms modality=%s items=%d "
"encoder_output_tokens=%d move_h2d=%.3f encode=%.3f "
"per_item_tokens=%s",
modality.name,
len(items),
int(output.shape[0]),
move_elapsed_ms,
encoder_elapsed_ms,
per_item_lens,
)
def _share_encoded_aliases(self, plan: EncodePlan) -> int:
released = 0
for canonical, aliases in plan.aliases_by_canonical.items():
if canonical.encoded is None:
continue
for alias in aliases:
alias.encoded = canonical.encoded
alias.encoded_deepstack = canonical.encoded_deepstack
if self._drop_raw_feature(alias):
released += 1
return released
# --- phase 3: assemble -------------------------------------------------
def _assemble(
self,
input_ids: torch.Tensor,
text_embedding: nn.Embedding,
plan: EncodePlan,
encoders: dict[Modality, EncoderSpec],
multimodal_model: nn.Module,
) -> tuple[torch.Tensor, dict[str, Any]]:
# Placeholder positions hold large content-derived IDs that exceed
# vocab_size; the lookup we run here is overwritten for those rows
# by the scatter below, but the lookup still needs valid indices.
vocab_size = text_embedding.num_embeddings
safe_ids = input_ids.clamp(min=0, max=vocab_size - 1)
input_embeds = text_embedding(safe_ids)
kwargs: dict[str, Any] = {}
deepstack_buffer: torch.Tensor | None = None
deepstack_modalities = {
modality for modality, spec in encoders.items() if spec.deepstack
}
if any(r.item.modality in deepstack_modalities for r in plan.scatter_ranges):
num_deepstack = len(multimodal_model.deepstack_visual_indexes)
shape = input_embeds.shape[:-1] + (input_embeds.shape[-1] * num_deepstack,)
deepstack_buffer = torch.zeros(
shape, dtype=input_embeds.dtype, device=input_embeds.device
)
kwargs["input_deepstack_embeds"] = deepstack_buffer
for r in plan.scatter_ranges:
main = r.item.encoded
if main is None:
raise RuntimeError(
"MultimodalEmbedder: item scheduled for encode has no "
"encoded tensor after _encode; this is a bug"
)
src = main[r.item_src_start : r.item_src_end + 1]
input_embeds[r.flat_dst_start : r.flat_dst_end + 1] = src.to(
dtype=input_embeds.dtype, device=input_embeds.device
)
if deepstack_buffer is not None and r.item.encoded_deepstack is not None:
deep_src = r.item.encoded_deepstack[
r.item_src_start : r.item_src_end + 1
]
deepstack_buffer[r.flat_dst_start : r.flat_dst_end + 1] = deep_src.to(
dtype=input_embeds.dtype, device=input_embeds.device
)
return input_embeds, kwargs
# --- device helpers ----------------------------------------------------
def _h2d_stream_on(self, device: torch.device) -> torch.cuda.Stream:
if self._h2d_stream is None:
self._h2d_stream = torch.cuda.Stream(device=device)
return self._h2d_stream
def _move_features_to_device(
self, items: list[MultimodalDataItem], device: torch.device
) -> None:
"""Stage encoder features onto ``device`` on a dedicated H2D stream.
Inputs that originate from the SHM transport are pinned, so the
H2D copy can actually run async with respect to the LM kernels
already queued on the current stream. We synchronise the current
stream with the H2D stream before returning so the encode call
sees the moved tensors.
"""
pending = [
it
for it in items
if isinstance(it.feature, (torch.Tensor, ShmTensorHandle))
and (isinstance(it.feature, ShmTensorHandle) or it.feature.device != device)
]
if not pending:
return
for it in pending:
if isinstance(it.feature, ShmTensorHandle):
it.feature = it.feature.consume()
if device.type != "cuda":
for it in pending:
if isinstance(it.feature, torch.Tensor):
it.feature = it.feature.to(device, non_blocking=True)
return
h2d = self._h2d_stream_on(device)
current = torch.cuda.current_stream(device)
with torch.cuda.stream(h2d):
for it in pending:
if isinstance(it.feature, torch.Tensor):
it.feature = it.feature.to(device, non_blocking=True)
current.wait_stream(h2d)
@staticmethod
def _drop_raw_feature(item: MultimodalDataItem) -> bool:
if item.feature is None:
return False
if isinstance(item.feature, ShmTensorHandle):
item.feature.release()
item.feature = None
return True
@staticmethod
def _drop_encoded_features(ctx: MultimodalForwardContext) -> int:
released = 0
for mm in ctx.mm_inputs:
if mm is None:
continue
for it in mm.mm_items:
if it.encoded is not None and MultimodalEmbedder._drop_raw_feature(it):
released += 1
return released
# Compatibility alias for model implementations that predate audio support.
VisionEmbedder = MultimodalEmbedder