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
644 lines
24 KiB
Python
644 lines
24 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.
|
|
|
|
"""Budget-bucketed CUDA graph capture/replay for multimodal encoders.
|
|
|
|
The :class:`EncoderCudaGraphWrapper` is the generic manager: it owns budget
|
|
selection, graph capture, replay buffer updates, greedy packing, and eager
|
|
fallback. Model/modality-specific input layout is supplied by an adapter object.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import contextlib
|
|
from collections.abc import Callable
|
|
from dataclasses import dataclass
|
|
from functools import cached_property
|
|
from typing import Any, Protocol
|
|
|
|
import torch
|
|
|
|
from tokenspeed.runtime.distributed.comm_backend.registry import get_global_backend
|
|
from tokenspeed.runtime.utils import logger
|
|
|
|
|
|
@dataclass
|
|
class BudgetGraphMetadata:
|
|
"""One captured budget graph.
|
|
|
|
Replay copies a real batch into the captured input/metadata buffers, calls
|
|
``graph.replay()``, then reads ``output_buffer``.
|
|
"""
|
|
|
|
graph: torch.cuda.CUDAGraph
|
|
input_buffers: dict[str, torch.Tensor]
|
|
metadata_buffers: dict[str, torch.Tensor]
|
|
output_buffer: torch.Tensor
|
|
|
|
|
|
class EncoderCudaGraphBatch(Protocol):
|
|
"""Minimal batch contract consumed by :class:`EncoderCudaGraphWrapper`."""
|
|
|
|
@property
|
|
def input_tensors(self) -> dict[str, torch.Tensor]: ...
|
|
|
|
@property
|
|
def encoder_output_tokens(self) -> list[int]: ...
|
|
|
|
@property
|
|
def metadata_sequences(self) -> list[int]: ...
|
|
|
|
def num_items(self) -> int: ...
|
|
|
|
def select(self, indices: list[int]) -> "EncoderCudaGraphBatch": ...
|
|
|
|
|
|
class EncoderCudaGraphAdapter(Protocol):
|
|
"""Model/modality-specific contract used by :class:`EncoderCudaGraphWrapper`."""
|
|
|
|
@property
|
|
def modality_name(self) -> str: ...
|
|
|
|
@property
|
|
def device(self) -> torch.device: ...
|
|
|
|
@property
|
|
def dtype(self) -> torch.dtype: ...
|
|
|
|
@property
|
|
def capture_tp_size(self) -> int: ...
|
|
|
|
@property
|
|
def capture_tp_group(self) -> Any | None: ...
|
|
|
|
def batch_from_items(self, items: list[Any]) -> EncoderCudaGraphBatch: ...
|
|
|
|
def capture_batch_for_budget(
|
|
self,
|
|
encoder_output_token_budget: int,
|
|
max_batch_size: int,
|
|
metadata_sequence_budget: int,
|
|
device: torch.device,
|
|
dtype: torch.dtype,
|
|
) -> EncoderCudaGraphBatch: ...
|
|
|
|
def prepare_metadata(
|
|
self,
|
|
batch: EncoderCudaGraphBatch,
|
|
encoder_output_token_budget: int | None,
|
|
metadata_sequence_budget: int,
|
|
) -> dict[str, Any]: ...
|
|
|
|
def forward(
|
|
self,
|
|
input_tensors: dict[str, torch.Tensor],
|
|
metadata: dict[str, Any],
|
|
) -> torch.Tensor: ...
|
|
|
|
def postprocess(
|
|
self,
|
|
encoder_outs: list[torch.Tensor],
|
|
batch: EncoderCudaGraphBatch,
|
|
) -> torch.Tensor: ...
|
|
|
|
|
|
@dataclass
|
|
class VisionEncoderBatch:
|
|
"""Qwen/Kimi-style vision batch.
|
|
|
|
Per-item patch rows are concatenated on dim 0 and indexed by ``grid_thw``.
|
|
``.tolist()`` syncs are confined here, in the eager region, never inside a
|
|
graph replay.
|
|
"""
|
|
|
|
tokens: torch.Tensor
|
|
grid: torch.Tensor
|
|
out_div: int
|
|
|
|
@property
|
|
def input_tensors(self) -> dict[str, torch.Tensor]:
|
|
return {"tokens": self.tokens}
|
|
|
|
@cached_property
|
|
def _grid_rows(self) -> list[list[int]]:
|
|
return self.grid.tolist()
|
|
|
|
def num_items(self) -> int:
|
|
return self.grid.shape[0]
|
|
|
|
@cached_property
|
|
def encoder_output_tokens(self) -> list[int]:
|
|
return [(t * h * w) // self.out_div for t, h, w in self._grid_rows]
|
|
|
|
@cached_property
|
|
def cu_input(self) -> list[int]:
|
|
cu = [0]
|
|
for t, h, w in self._grid_rows:
|
|
cu.append(cu[-1] + t * h * w)
|
|
return cu
|
|
|
|
@cached_property
|
|
def metadata_sequences(self) -> list[int]:
|
|
return [t for t, _, _ in self._grid_rows]
|
|
|
|
def select(self, indices: list[int]) -> "VisionEncoderBatch":
|
|
"""Sub-batch at ``indices``, preserving order."""
|
|
cu = self.cu_input
|
|
if indices:
|
|
rows = torch.cat(
|
|
[
|
|
torch.arange(cu[i], cu[i + 1], device=self.tokens.device)
|
|
for i in indices
|
|
]
|
|
)
|
|
else:
|
|
rows = torch.zeros(0, dtype=torch.long, device=self.tokens.device)
|
|
return VisionEncoderBatch(self.tokens[rows], self.grid[indices], self.out_div)
|
|
|
|
|
|
@dataclass
|
|
class VisionEncoderCudaGraphAdapter:
|
|
"""Adapter for Qwen/Kimi-style ``grid_thw`` vision encoders."""
|
|
|
|
tower: Any
|
|
pre_encode: Callable[[list[Any]], tuple[torch.Tensor, torch.Tensor]]
|
|
post_encode: Callable[[list[torch.Tensor], torch.Tensor], torch.Tensor]
|
|
out_div: int
|
|
merge: int
|
|
input_feature_shape: tuple[int, ...]
|
|
modality_name: str = "vision"
|
|
out_squeeze_dim: int | None = None
|
|
capture_tp_size: int = 1
|
|
capture_tp_group: Any | None = None
|
|
|
|
@cached_property
|
|
def _param(self) -> torch.nn.Parameter:
|
|
return next(self.tower.parameters())
|
|
|
|
@property
|
|
def device(self) -> torch.device:
|
|
return self._param.device
|
|
|
|
@property
|
|
def dtype(self) -> torch.dtype:
|
|
return self._param.dtype
|
|
|
|
def synthetic_grid(self, encoder_output_token_budget: int) -> list[list[int]]:
|
|
n_patches = encoder_output_token_budget * self.out_div
|
|
units = max(n_patches // (self.merge * self.merge), 1)
|
|
a = 1 << (units.bit_length() // 2)
|
|
while a > 1 and units % a != 0:
|
|
a >>= 1
|
|
b = units // a
|
|
return [[1, a * self.merge, b * self.merge]]
|
|
|
|
def pad_cu_seqlens(
|
|
self, metadata: dict[str, Any], metadata_sequence_budget: int
|
|
) -> None:
|
|
cu = metadata["cu_seqlens"]
|
|
target = metadata_sequence_budget + 1
|
|
if cu.shape[0] > target:
|
|
raise RuntimeError(
|
|
f"{self.modality_name} encoder cudagraph needs {cu.shape[0] - 1} "
|
|
f"metadata sequences, but the configured limit is "
|
|
f"{metadata_sequence_budget}"
|
|
)
|
|
pad = target - cu.shape[0]
|
|
if pad > 0:
|
|
metadata["cu_seqlens"] = torch.cat([cu, cu[-1:].expand(pad)])
|
|
|
|
def batch_from_items(self, items: list[Any]) -> VisionEncoderBatch:
|
|
tokens, grid = self.pre_encode(items)
|
|
return VisionEncoderBatch(tokens, grid, self.out_div)
|
|
|
|
def capture_batch_for_budget(
|
|
self,
|
|
encoder_output_token_budget: int,
|
|
_max_batch_size: int,
|
|
_metadata_sequence_budget: int,
|
|
capture_device: torch.device,
|
|
capture_dtype: torch.dtype,
|
|
) -> VisionEncoderBatch:
|
|
grid = torch.tensor(
|
|
self.synthetic_grid(encoder_output_token_budget),
|
|
device=capture_device,
|
|
dtype=torch.int32,
|
|
)
|
|
tokens = torch.zeros(
|
|
(encoder_output_token_budget * self.out_div, *self.input_feature_shape),
|
|
device=capture_device,
|
|
dtype=capture_dtype,
|
|
)
|
|
return VisionEncoderBatch(tokens, grid, self.out_div)
|
|
|
|
def prepare_metadata(
|
|
self,
|
|
batch: EncoderCudaGraphBatch,
|
|
encoder_output_token_budget: int | None,
|
|
metadata_sequence_budget: int,
|
|
) -> dict[str, Any]:
|
|
if not isinstance(batch, VisionEncoderBatch):
|
|
raise TypeError(
|
|
f"{self.modality_name} encoder cudagraph expected "
|
|
f"VisionEncoderBatch, got {type(batch).__name__}"
|
|
)
|
|
metadata = dict(self.tower.prepare_metadata(batch.grid))
|
|
if encoder_output_token_budget is not None:
|
|
self.pad_cu_seqlens(metadata, metadata_sequence_budget)
|
|
# Non-tensor scalar gets baked at capture. Use the per-budget worst
|
|
# case so replay never exceeds the captured attention max seqlen.
|
|
metadata["max_seqlen"] = encoder_output_token_budget * self.out_div
|
|
return metadata
|
|
|
|
def forward(
|
|
self, input_tensors: dict[str, torch.Tensor], metadata: dict[str, Any]
|
|
) -> torch.Tensor:
|
|
out = self.tower.forward_blocks(input_tensors["tokens"], metadata)
|
|
if self.out_squeeze_dim is not None:
|
|
out = out.squeeze(self.out_squeeze_dim)
|
|
return out
|
|
|
|
def postprocess(
|
|
self, encoder_outs: list[torch.Tensor], batch: EncoderCudaGraphBatch
|
|
) -> torch.Tensor:
|
|
if not isinstance(batch, VisionEncoderBatch):
|
|
raise TypeError(
|
|
f"{self.modality_name} encoder cudagraph expected "
|
|
f"VisionEncoderBatch, got {type(batch).__name__}"
|
|
)
|
|
return self.post_encode(encoder_outs, batch.grid)
|
|
|
|
|
|
class EncoderCudaGraphWrapper:
|
|
"""Generic budget-based CUDA graph manager for encoder callables.
|
|
|
|
The wrapper does not know about image/video/audio internals. It only expects
|
|
batches to expose input tensors, per-item output row counts, per-item
|
|
metadata sequence counts, and a ``select`` operation. The adapter constructs
|
|
batches, prepares metadata, runs the captured forward, and postprocesses
|
|
output slices.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
*,
|
|
adapter: EncoderCudaGraphAdapter,
|
|
budget_range: tuple[int, int],
|
|
max_batch_size: int | None = None,
|
|
max_metadata_sequences_per_batch: int | None = None,
|
|
metadata_sequence_budget_from_encoder_output_budget: bool = False,
|
|
):
|
|
self.adapter = adapter
|
|
self.device = adapter.device
|
|
self.dtype = adapter.dtype
|
|
self.modality_name = adapter.modality_name
|
|
|
|
min_budget, max_budget = budget_range
|
|
self.encoder_output_token_budgets = self._generate_budgets(
|
|
min_budget, max_budget
|
|
)
|
|
self.max_batch_size = (
|
|
max_batch_size
|
|
if max_batch_size is not None
|
|
else max(1, max_budget // max(1, min_budget))
|
|
)
|
|
self.max_metadata_sequences_per_batch = max_metadata_sequences_per_batch
|
|
self.metadata_sequence_budget_from_encoder_output_budget = (
|
|
metadata_sequence_budget_from_encoder_output_budget
|
|
)
|
|
|
|
self.capture_tp_size = adapter.capture_tp_size
|
|
self.capture_tp_group = adapter.capture_tp_group
|
|
|
|
self.budget_graphs: dict[int, BudgetGraphMetadata] = {}
|
|
|
|
metadata_sequence_budget_log_value = (
|
|
self.max_metadata_sequences_per_batch
|
|
if self.max_metadata_sequences_per_batch is not None
|
|
else (
|
|
"encoder_output_token_budget"
|
|
if self.metadata_sequence_budget_from_encoder_output_budget
|
|
else "batch"
|
|
)
|
|
)
|
|
logger.info(
|
|
"EncoderCudaGraphWrapper initialized: modality=%s, budgets=%s, "
|
|
"max_batch_size=%d, max_metadata_sequences_per_batch=%s, encoder_tp=%d",
|
|
self.modality_name,
|
|
self.encoder_output_token_budgets,
|
|
self.max_batch_size,
|
|
metadata_sequence_budget_log_value,
|
|
self.capture_tp_size,
|
|
)
|
|
|
|
def __call__(self, items: list[Any]) -> torch.Tensor:
|
|
batch = self.adapter.batch_from_items(items)
|
|
if not self.budget_graphs:
|
|
self.capture()
|
|
encoder_outs = self._dispatch(batch)
|
|
return self.adapter.postprocess(encoder_outs, batch)
|
|
|
|
@staticmethod
|
|
def _generate_budgets(min_budget: int, max_budget: int) -> list[int]:
|
|
"""Power-of-2 budgets in ``[min_budget, max_budget]``."""
|
|
budgets: list[int] = []
|
|
b = max(1, min_budget)
|
|
while b <= max_budget:
|
|
budgets.append(b)
|
|
b *= 2
|
|
if not budgets or budgets[-1] < max_budget:
|
|
budgets.append(max_budget)
|
|
return budgets
|
|
|
|
def _metadata_sequence_budget_for_encoder_output_budget(
|
|
self, encoder_output_token_budget: int
|
|
) -> int:
|
|
if self.max_metadata_sequences_per_batch is not None:
|
|
return min(
|
|
self.max_metadata_sequences_per_batch, encoder_output_token_budget
|
|
)
|
|
if self.metadata_sequence_budget_from_encoder_output_budget:
|
|
return encoder_output_token_budget
|
|
return self.max_batch_size
|
|
|
|
def capture(self) -> None:
|
|
for encoder_output_token_budget in self.encoder_output_token_budgets:
|
|
self._capture_one(encoder_output_token_budget)
|
|
logger.info(
|
|
"Encoder CUDA graph capture complete: modality=%s, %d budget graphs.",
|
|
self.modality_name,
|
|
len(self.budget_graphs),
|
|
)
|
|
|
|
def _capture_one(self, encoder_output_token_budget: int) -> None:
|
|
metadata_sequence_budget = (
|
|
self._metadata_sequence_budget_for_encoder_output_budget(
|
|
encoder_output_token_budget
|
|
)
|
|
)
|
|
batch = self.adapter.capture_batch_for_budget(
|
|
encoder_output_token_budget,
|
|
self.max_batch_size,
|
|
metadata_sequence_budget,
|
|
self.device,
|
|
self.dtype,
|
|
)
|
|
metadata = dict(
|
|
self.adapter.prepare_metadata(
|
|
batch, encoder_output_token_budget, metadata_sequence_budget
|
|
)
|
|
)
|
|
input_buffers = batch.input_tensors
|
|
|
|
# Warmup also forces lazy JIT / autotune before capture.
|
|
with torch.inference_mode():
|
|
output = self.adapter.forward(input_buffers, metadata)
|
|
output_buffer = torch.empty_like(output)
|
|
|
|
# Encoder TP > 1: capture must record per-layer all-reduce under the
|
|
# custom-AR capture context.
|
|
if self.capture_tp_size > 1 and self.capture_tp_group is not None:
|
|
ar_ctx: Any = get_global_backend().custom_ar.capture(self.capture_tp_group)
|
|
else:
|
|
ar_ctx = contextlib.nullcontext()
|
|
|
|
# No pool= argument: each budget graph gets its own private pool. A
|
|
# shared pool collides custom-AR IPC registrations across budgets.
|
|
graph = torch.cuda.CUDAGraph()
|
|
with torch.inference_mode(), ar_ctx, torch.cuda.graph(graph):
|
|
output = self.adapter.forward(input_buffers, metadata)
|
|
output_buffer.copy_(output)
|
|
|
|
# Only tensor entries are captured. Ints / None are baked at capture.
|
|
metadata_buffers = {
|
|
k: v for k, v in metadata.items() if isinstance(v, torch.Tensor)
|
|
}
|
|
self.budget_graphs[encoder_output_token_budget] = BudgetGraphMetadata(
|
|
graph=graph,
|
|
input_buffers=input_buffers,
|
|
metadata_buffers=metadata_buffers,
|
|
output_buffer=output_buffer,
|
|
)
|
|
logger.debug(
|
|
"Captured encoder cudagraph: modality=%s, budget=%d, "
|
|
"max_batch_size=%d, metadata_sequence_budget=%d, buffers=%s",
|
|
self.modality_name,
|
|
encoder_output_token_budget,
|
|
self.max_batch_size,
|
|
metadata_sequence_budget,
|
|
{k: (v.dtype, tuple(v.shape)) for k, v in metadata_buffers.items()},
|
|
)
|
|
|
|
def _smallest_fitting_budget(
|
|
self, total_encoder_output_tokens: int, total_metadata_sequences: int
|
|
) -> int | None:
|
|
for budget in self.encoder_output_token_budgets:
|
|
if (
|
|
budget >= total_encoder_output_tokens
|
|
and total_metadata_sequences
|
|
<= self._metadata_sequence_budget_for_encoder_output_budget(budget)
|
|
):
|
|
return budget
|
|
return None
|
|
|
|
@staticmethod
|
|
def _scatter_output_slices(
|
|
output: torch.Tensor,
|
|
indices: list[int],
|
|
per_item_encoder_output_tokens: list[int],
|
|
dest: dict[int, torch.Tensor],
|
|
clone: bool = False,
|
|
) -> None:
|
|
"""Slice ``output`` and scatter into ``dest`` by original item index."""
|
|
offset = 0
|
|
for idx in indices:
|
|
n_tokens = per_item_encoder_output_tokens[idx]
|
|
sliced = output[offset : offset + n_tokens]
|
|
dest[idx] = sliced.clone() if clone else sliced
|
|
offset += n_tokens
|
|
|
|
def _run_budget_graph(
|
|
self,
|
|
batch: EncoderCudaGraphBatch,
|
|
encoder_output_token_budget: int,
|
|
) -> torch.Tensor:
|
|
"""Copy the batch into captured buffers, replay, and return output."""
|
|
graph_meta = self.budget_graphs[encoder_output_token_budget]
|
|
metadata_sequence_budget = (
|
|
self._metadata_sequence_budget_for_encoder_output_budget(
|
|
encoder_output_token_budget
|
|
)
|
|
)
|
|
|
|
src_buffers = batch.input_tensors
|
|
if src_buffers.keys() != graph_meta.input_buffers.keys():
|
|
raise RuntimeError(
|
|
f"{self.modality_name} encoder cudagraph input keys changed: "
|
|
f"capture={sorted(graph_meta.input_buffers.keys())}, "
|
|
f"replay={sorted(src_buffers.keys())}"
|
|
)
|
|
|
|
for key, buf in graph_meta.input_buffers.items():
|
|
src = src_buffers[key]
|
|
n = src.shape[0]
|
|
if n > buf.shape[0]:
|
|
raise RuntimeError(
|
|
f"{self.modality_name} encoder cudagraph input {key} has "
|
|
f"{n} rows, but budget {encoder_output_token_budget} only "
|
|
f"captured {buf.shape[0]} rows"
|
|
)
|
|
if src.shape[1:] != buf.shape[1:]:
|
|
raise RuntimeError(
|
|
f"{self.modality_name} encoder cudagraph input {key} "
|
|
f"shape changed after dim0: capture={tuple(buf.shape)}, "
|
|
f"replay={tuple(src.shape)}"
|
|
)
|
|
buf.zero_()
|
|
buf[:n].copy_(src)
|
|
|
|
metadata = dict(
|
|
self.adapter.prepare_metadata(
|
|
batch, encoder_output_token_budget, metadata_sequence_budget
|
|
)
|
|
)
|
|
replay_buffers = {
|
|
k: v for k, v in metadata.items() if isinstance(v, torch.Tensor)
|
|
}
|
|
|
|
if replay_buffers.keys() != graph_meta.metadata_buffers.keys():
|
|
raise RuntimeError(
|
|
f"{self.modality_name} encoder cudagraph metadata keys changed: "
|
|
f"capture={sorted(graph_meta.metadata_buffers.keys())}, "
|
|
f"replay={sorted(replay_buffers.keys())}"
|
|
)
|
|
|
|
for key, buf in graph_meta.metadata_buffers.items():
|
|
new = replay_buffers[key]
|
|
if new.ndim == 0:
|
|
buf.copy_(new)
|
|
else:
|
|
if new.shape[1:] != buf.shape[1:]:
|
|
raise RuntimeError(
|
|
f"{self.modality_name} encoder cudagraph metadata {key} "
|
|
f"shape changed after dim0: capture={tuple(buf.shape)}, "
|
|
f"replay={tuple(new.shape)}"
|
|
)
|
|
if new.shape[0] > buf.shape[0]:
|
|
raise RuntimeError(
|
|
f"{self.modality_name} encoder cudagraph metadata {key} "
|
|
f"has {new.shape[0]} rows, but the captured buffer only "
|
|
f"has {buf.shape[0]} rows"
|
|
)
|
|
buf.zero_()
|
|
buf[: new.shape[0]].copy_(new)
|
|
|
|
graph_meta.graph.replay()
|
|
return graph_meta.output_buffer
|
|
|
|
def _run_eager(self, batch: EncoderCudaGraphBatch) -> torch.Tensor:
|
|
metadata = dict(
|
|
self.adapter.prepare_metadata(
|
|
batch, None, max(1, sum(batch.metadata_sequences))
|
|
)
|
|
)
|
|
return self.adapter.forward(batch.input_tensors, metadata)
|
|
|
|
def _dispatch(self, batch: EncoderCudaGraphBatch) -> list[torch.Tensor]:
|
|
"""Greedy smallest-first pack into budget graphs with eager fallback."""
|
|
num_items = batch.num_items()
|
|
max_budget = self.encoder_output_token_budgets[-1]
|
|
max_metadata_sequence_budget = (
|
|
self._metadata_sequence_budget_for_encoder_output_budget(max_budget)
|
|
)
|
|
per_item_encoder_output_tokens = batch.encoder_output_tokens
|
|
per_item_metadata_sequences = batch.metadata_sequences
|
|
|
|
sorted_indices = sorted(
|
|
range(num_items), key=lambda i: per_item_encoder_output_tokens[i]
|
|
)
|
|
|
|
batches: list[tuple[list[int], int | None]] = []
|
|
current_batch: list[int] = []
|
|
current_batch_encoder_output_tokens = 0
|
|
current_batch_metadata_sequences = 0
|
|
for orig_idx in sorted_indices:
|
|
item_encoder_output_tokens = per_item_encoder_output_tokens[orig_idx]
|
|
item_metadata_sequences = per_item_metadata_sequences[orig_idx]
|
|
if (
|
|
current_batch_encoder_output_tokens + item_encoder_output_tokens
|
|
<= max_budget
|
|
and len(current_batch) < self.max_batch_size
|
|
and current_batch_metadata_sequences + item_metadata_sequences
|
|
<= max_metadata_sequence_budget
|
|
):
|
|
current_batch.append(orig_idx)
|
|
current_batch_encoder_output_tokens += item_encoder_output_tokens
|
|
current_batch_metadata_sequences += item_metadata_sequences
|
|
else:
|
|
if current_batch:
|
|
batches.append(
|
|
(
|
|
current_batch,
|
|
self._smallest_fitting_budget(
|
|
current_batch_encoder_output_tokens,
|
|
current_batch_metadata_sequences,
|
|
),
|
|
)
|
|
)
|
|
current_batch = [orig_idx]
|
|
current_batch_encoder_output_tokens = item_encoder_output_tokens
|
|
current_batch_metadata_sequences = item_metadata_sequences
|
|
if current_batch:
|
|
batches.append(
|
|
(
|
|
current_batch,
|
|
self._smallest_fitting_budget(
|
|
current_batch_encoder_output_tokens,
|
|
current_batch_metadata_sequences,
|
|
),
|
|
)
|
|
)
|
|
|
|
# Packing reorders; restore original order before return.
|
|
outputs_by_orig_idx: dict[int, torch.Tensor] = {}
|
|
for batch_orig_indices, encoder_output_token_budget in batches:
|
|
sub_batch = batch.select(batch_orig_indices)
|
|
if encoder_output_token_budget is None:
|
|
with torch.inference_mode():
|
|
raw = self._run_eager(sub_batch)
|
|
self._scatter_output_slices(
|
|
raw,
|
|
batch_orig_indices,
|
|
per_item_encoder_output_tokens,
|
|
outputs_by_orig_idx,
|
|
)
|
|
else:
|
|
output = self._run_budget_graph(sub_batch, encoder_output_token_budget)
|
|
# clone: output is the shared, reused output_buffer.
|
|
self._scatter_output_slices(
|
|
output,
|
|
batch_orig_indices,
|
|
per_item_encoder_output_tokens,
|
|
outputs_by_orig_idx,
|
|
clone=True,
|
|
)
|
|
|
|
return [outputs_by_orig_idx[i] for i in range(num_items)]
|