mirror of
https://github.com/vllm-project/vllm.git
synced 2026-08-04 04:48:07 +00:00
[MM][Perf][CG] Support ViT full CUDA graph for Kimi-VL (#41992)
Signed-off-by: oguz <[email protected]>
This commit is contained in:
@@ -129,6 +129,7 @@ Models opt-in to encoder CUDA Graphs by implementing the [SupportsEncoderCudaGra
|
||||
| `DeepseekOCRForCausalLM` | `DeepSeek-OCR` | ✅︎ | ❌︎ | ✅︎ |
|
||||
| `Glm4vForConditionalGeneration` | `GLM-4.1V, GLM-4.6V-Flash` | ✅︎ | ✅︎ | ❌︎ |
|
||||
| `InternVLChatModel` | `InternVL3.5`, `InternVL3`, `InternVL2.5`, `InternVL2` | ✅︎ | ✅︎ | ❌︎ |
|
||||
| `KimiVLForConditionalGeneration` | `Kimi-VL` | ✅︎ | ❌︎ | ❌︎ |
|
||||
| `Llama4ForConditionalGeneration` | `Llama 4` | ✅︎ | ❌︎ | ❌︎ |
|
||||
| `Qwen2VLForConditionalGeneration` | `Qwen2-VL` | ✅︎ | ✅︎ | ❌︎ |
|
||||
| `Qwen2_5_VLForConditionalGeneration` | `Qwen2.5-VL` | ✅︎ | ✅︎ | ❌︎ |
|
||||
|
||||
@@ -2537,6 +2537,7 @@ MODELS_SUPPORT_VIT_CUDA_GRAPH = [
|
||||
"qwen2_5_vl",
|
||||
"qwen3_vl",
|
||||
"qwen3_vl_moe",
|
||||
"kimi_vl",
|
||||
"qwen3_5",
|
||||
"qwen3_5_moe",
|
||||
"internvl_chat",
|
||||
|
||||
@@ -48,6 +48,13 @@ def internvl_chat_template(content: str) -> str:
|
||||
return f"<|im_start|>user\n{content}<|im_end|>\n<|im_start|>assistant\n"
|
||||
|
||||
|
||||
def kimi_vl_chat_template(content: str) -> str:
|
||||
return (
|
||||
f"<|im_user|>user<|im_middle|>{content}<|im_end|>"
|
||||
"<|im_assistant|>assistant<|im_middle|>"
|
||||
)
|
||||
|
||||
|
||||
def step3_vl_chat_template(content: str) -> str:
|
||||
return (
|
||||
"<|begin▁of▁sentence|> You are a helpful assistant.<|BOT|>user\n "
|
||||
@@ -100,6 +107,34 @@ MODEL_CONFIGS: dict[str, VitCudagraphTestConfig] = {
|
||||
needs_video_metadata=False,
|
||||
marks=[pytest.mark.core_model],
|
||||
),
|
||||
"kimi_vl": VitCudagraphTestConfig(
|
||||
model="moonshotai/Kimi-VL-A3B-Instruct",
|
||||
modalities=["image"],
|
||||
image_prompt=kimi_vl_chat_template(
|
||||
"<|media_start|>image<|media_content|><|media_pad|><|media_end|>"
|
||||
"What is in this image?"
|
||||
),
|
||||
needs_video_metadata=False,
|
||||
# Single bucket sized to cover the test images' output tokens.
|
||||
# The default auto-inferred range fans out into multiple power-of-2
|
||||
# buckets, each holding a full ViT capture pool.
|
||||
compilation_config_overrides={
|
||||
"encoder_cudagraph_token_budgets": [1024],
|
||||
},
|
||||
# Shrink to 1 text + 1 vision layer with random weights so the
|
||||
# test runs on any CI GPU (incl. L4) and skips the multi-GiB
|
||||
# weight download. The test only validates that encoder CG
|
||||
# capture/replay functions correctly, not output quality.
|
||||
vllm_runner_kwargs={
|
||||
"trust_remote_code": True,
|
||||
"load_format": "dummy",
|
||||
"hf_overrides": partial(
|
||||
dummy_hf_overrides,
|
||||
model_arch="KimiVLForConditionalGeneration",
|
||||
),
|
||||
},
|
||||
marks=[pytest.mark.core_model],
|
||||
),
|
||||
"qwen3_vl": VitCudagraphTestConfig(
|
||||
model="Qwen/Qwen3-VL-2B-Instruct",
|
||||
image_prompt=qwen_vl_chat_template(
|
||||
|
||||
@@ -56,7 +56,11 @@ from vllm.config import VllmConfig
|
||||
from vllm.config.multimodal import BaseDummyOptions
|
||||
from vllm.inputs import MultiModalDataDict
|
||||
from vllm.model_executor.layers.linear import ReplicatedLinear
|
||||
from vllm.model_executor.models.interfaces import SupportsMultiModal, SupportsPP
|
||||
from vllm.model_executor.models.interfaces import (
|
||||
SupportsEncoderCudaGraph,
|
||||
SupportsMultiModal,
|
||||
SupportsPP,
|
||||
)
|
||||
from vllm.model_executor.models.moonvit import MoonVitPretrainedModel
|
||||
from vllm.multimodal import MULTIMODAL_REGISTRY
|
||||
from vllm.multimodal.inputs import (
|
||||
@@ -79,6 +83,7 @@ from vllm.multimodal.processing import (
|
||||
from vllm.sequence import IntermediateTensors
|
||||
from vllm.transformers_utils.configs.kimi_vl import KimiVLConfig, MoonViTConfig
|
||||
from vllm.utils.tensor_schema import TensorSchema, TensorShape
|
||||
from vllm.v1.worker.encoder_cudagraph_defs import EncoderCudaGraphReplayBuffers
|
||||
|
||||
from .utils import AutoWeightsLoader, init_vllm_registered_model, maybe_prefix
|
||||
from .vision import is_vit_use_data_parallel, run_dp_sharded_mrope_vision_model
|
||||
@@ -287,7 +292,9 @@ class KimiVLMultiModalProcessor(BaseMultiModalProcessor[KimiVLProcessingInfo]):
|
||||
info=KimiVLProcessingInfo,
|
||||
dummy_inputs=KimiVLDummyInputsBuilder,
|
||||
)
|
||||
class KimiVLForConditionalGeneration(nn.Module, SupportsMultiModal, SupportsPP):
|
||||
class KimiVLForConditionalGeneration(
|
||||
nn.Module, SupportsMultiModal, SupportsEncoderCudaGraph, SupportsPP
|
||||
):
|
||||
supports_encoder_tp_data = True
|
||||
|
||||
@classmethod
|
||||
@@ -340,6 +347,192 @@ class KimiVLForConditionalGeneration(nn.Module, SupportsMultiModal, SupportsPP):
|
||||
|
||||
self.media_placeholder: int = self.config.media_placeholder_token_id
|
||||
|
||||
self.model_config = model_config
|
||||
|
||||
# -- SupportsEncoderCudaGraph protocol methods --
|
||||
|
||||
def get_encoder_cudagraph_config(self):
|
||||
from vllm.v1.worker.encoder_cudagraph_defs import (
|
||||
EncoderCudaGraphConfig,
|
||||
)
|
||||
|
||||
return EncoderCudaGraphConfig(
|
||||
modalities=["image"],
|
||||
buffer_keys=[
|
||||
"pixel_values",
|
||||
"pos_embeds",
|
||||
"rope_freqs_cis",
|
||||
"cu_seqlens",
|
||||
"max_seqlen",
|
||||
"merge_gather_idx",
|
||||
],
|
||||
out_hidden_size=self.hidden_size,
|
||||
)
|
||||
|
||||
def get_encoder_cudagraph_budget_range(
|
||||
self,
|
||||
vllm_config,
|
||||
) -> tuple[int, int]:
|
||||
# Min: estimated smallest possible encoder input.
|
||||
# 224x224 image with patch_size=14 -> 16x16 patches, then merge
|
||||
# kernel (2,2) -> 8x8 = 64 output tokens.
|
||||
min_budget = 64
|
||||
max_budget = min(
|
||||
vllm_config.scheduler_config.max_num_batched_tokens,
|
||||
self.model_config.max_model_len,
|
||||
)
|
||||
return (min_budget, max_budget)
|
||||
|
||||
def _get_grid_hws(
|
||||
self,
|
||||
mm_kwargs: dict[str, Any],
|
||||
) -> list[tuple[int, int]]:
|
||||
grid_hws = mm_kwargs["image_grid_hws"]
|
||||
if not isinstance(grid_hws, list):
|
||||
grid_hws = grid_hws.tolist()
|
||||
return grid_hws
|
||||
|
||||
def get_encoder_cudagraph_item_specs(
|
||||
self,
|
||||
mm_kwargs: dict[str, Any],
|
||||
):
|
||||
from vllm.v1.worker.encoder_cudagraph_defs import EncoderItemSpec
|
||||
|
||||
kh, kw = self.config.vision_config.merge_kernel_size
|
||||
return [
|
||||
EncoderItemSpec(
|
||||
input_size=h * w,
|
||||
output_tokens=(h // kh) * (w // kw),
|
||||
)
|
||||
for h, w in self._get_grid_hws(mm_kwargs)
|
||||
]
|
||||
|
||||
def select_encoder_cudagraph_items(
|
||||
self,
|
||||
mm_kwargs: dict[str, Any],
|
||||
indices: list[int],
|
||||
) -> dict[str, Any]:
|
||||
grid_hws = self._get_grid_hws(mm_kwargs)
|
||||
pixel_values = mm_kwargs["pixel_values"]
|
||||
|
||||
if len(indices) == 0:
|
||||
return {
|
||||
"pixel_values": pixel_values[:0],
|
||||
"image_grid_hws": pixel_values.new_zeros((0, 2), dtype=torch.long),
|
||||
}
|
||||
|
||||
patches_per_item = [h * w for h, w in grid_hws]
|
||||
cum_patches = [0]
|
||||
for p in patches_per_item:
|
||||
cum_patches.append(cum_patches[-1] + p)
|
||||
|
||||
selected_pv = torch.cat(
|
||||
[pixel_values[cum_patches[i] : cum_patches[i + 1]] for i in indices]
|
||||
)
|
||||
selected_grid = torch.tensor(
|
||||
[grid_hws[i] for i in indices],
|
||||
dtype=torch.long,
|
||||
device=pixel_values.device,
|
||||
)
|
||||
return {
|
||||
"pixel_values": selected_pv,
|
||||
"image_grid_hws": selected_grid,
|
||||
}
|
||||
|
||||
def prepare_encoder_cudagraph_capture_inputs(
|
||||
self,
|
||||
token_budget: int,
|
||||
max_batch_size: int,
|
||||
max_frames_per_batch: int,
|
||||
device: torch.device,
|
||||
dtype: torch.dtype,
|
||||
path: str = "default",
|
||||
):
|
||||
from vllm.v1.worker.encoder_cudagraph_defs import (
|
||||
EncoderCudaGraphCaptureInputs,
|
||||
)
|
||||
|
||||
kh, kw = self.config.vision_config.merge_kernel_size
|
||||
# Ceil so the buffer fits the worst case of one item using the full
|
||||
# budget. Floor under-allocates when budget is not a multiple of
|
||||
# max_batch_size.
|
||||
per_mm_item_output = (token_budget + max_batch_size - 1) // max_batch_size
|
||||
|
||||
# Shape the synthetic grid so neither dimension exceeds Rope2DPosEmb's
|
||||
# precomputed range. Pack as wide a row as fits, then add rows.
|
||||
rope = self.vision_tower.encoder.rope_2d
|
||||
max_wo = rope.max_width // kw
|
||||
wo = min(per_mm_item_output, max_wo)
|
||||
ho = (per_mm_item_output + wo - 1) // wo
|
||||
assert ho * kh <= rope.max_height, (
|
||||
f"per_mm_item_output={per_mm_item_output} exceeds RoPE grid capacity "
|
||||
f"(max {(rope.max_height // kh) * (rope.max_width // kw)} tokens)"
|
||||
)
|
||||
grid_hws_list = [(ho * kh, wo * kw) for _ in range(max_batch_size)]
|
||||
|
||||
patch_size = self.config.vision_config.patch_size
|
||||
if isinstance(patch_size, int):
|
||||
patch_size = (patch_size, patch_size)
|
||||
|
||||
total_patches = sum(h * w for h, w in grid_hws_list)
|
||||
in_channels = 3
|
||||
dummy_pixel_values = torch.randn(
|
||||
total_patches,
|
||||
in_channels,
|
||||
patch_size[0],
|
||||
patch_size[1],
|
||||
device=device,
|
||||
dtype=dtype,
|
||||
)
|
||||
|
||||
buffers = self.vision_tower.prepare_encoder_metadata(
|
||||
grid_hws_list,
|
||||
max_batch_size=max_batch_size,
|
||||
max_seqlen_override=token_budget,
|
||||
device=device,
|
||||
)
|
||||
values = buffers | {"pixel_values": dummy_pixel_values}
|
||||
|
||||
return EncoderCudaGraphCaptureInputs(values=values)
|
||||
|
||||
def prepare_encoder_cudagraph_replay_buffers(
|
||||
self,
|
||||
mm_kwargs: dict[str, Any],
|
||||
max_batch_size: int,
|
||||
max_frames_per_batch: int,
|
||||
path: str = "default",
|
||||
):
|
||||
grid_hws_list = self._get_grid_hws(mm_kwargs)
|
||||
buffers = self.vision_tower.prepare_encoder_metadata(
|
||||
grid_hws_list,
|
||||
max_batch_size=max_batch_size,
|
||||
device=mm_kwargs["pixel_values"].device,
|
||||
)
|
||||
values = buffers | {"pixel_values": mm_kwargs["pixel_values"]}
|
||||
return EncoderCudaGraphReplayBuffers(values=values)
|
||||
|
||||
def encoder_cudagraph_forward(
|
||||
self,
|
||||
values: dict[str, torch.Tensor],
|
||||
path: str = "default",
|
||||
) -> torch.Tensor:
|
||||
pixel_values = values.pop("pixel_values")
|
||||
metadata = values
|
||||
image_features = self.vision_tower(
|
||||
pixel_values, grid_hw=None, encoder_metadata=metadata
|
||||
)
|
||||
return self.multi_modal_projector(image_features)
|
||||
|
||||
def encoder_eager_forward(
|
||||
self,
|
||||
mm_kwargs: dict[str, Any],
|
||||
path: str = "default",
|
||||
) -> torch.Tensor:
|
||||
pixel_values = mm_kwargs["pixel_values"]
|
||||
image_grid_hws = mm_kwargs["image_grid_hws"]
|
||||
image_features = self.vision_tower(pixel_values, image_grid_hws)
|
||||
return self.multi_modal_projector(torch.cat(image_features))
|
||||
|
||||
def _parse_and_validate_image_input(
|
||||
self, **kwargs: object
|
||||
) -> KimiVLImageInputs | None:
|
||||
|
||||
@@ -45,7 +45,9 @@
|
||||
from collections.abc import Sequence
|
||||
from copy import deepcopy
|
||||
from functools import cached_property
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
@@ -110,23 +112,42 @@ class Learnable2DInterpPosEmb(nn.Module):
|
||||
def reset_parameters(self):
|
||||
nn.init.normal_(self.weight)
|
||||
|
||||
def forward(self, x: torch.Tensor, grid_hws: torch.Tensor) -> torch.Tensor:
|
||||
pos_embs = []
|
||||
for shape in grid_hws.tolist():
|
||||
if shape == self.weight.shape[:-1]:
|
||||
def get_pos_embeds(
|
||||
self,
|
||||
grid_hws_list: list[list[int]] | list[tuple[int, int]],
|
||||
) -> torch.Tensor:
|
||||
"""Build packed per-token positional embeddings for a list of grids.
|
||||
|
||||
Returns a tensor of shape ``(sum(h * w), dim)`` formed by interpolating
|
||||
the learned ``(height, width, dim)`` weight to each ``(h, w)`` grid and
|
||||
concatenating the flattened results in the same order as
|
||||
``grid_hws_list``. Lives outside the captured CUDA graph so the
|
||||
per-grid Python iteration is safe.
|
||||
"""
|
||||
weight_shape = list(self.weight.shape[:-1])
|
||||
pos_embs: list[torch.Tensor] = []
|
||||
for shape in grid_hws_list:
|
||||
shape_list = [int(shape[0]), int(shape[1])]
|
||||
if shape_list == weight_shape:
|
||||
pos_embs.append(self.weight.flatten(end_dim=1))
|
||||
else:
|
||||
pos_embs.append(
|
||||
F.interpolate(
|
||||
self.weight.permute((2, 0, 1)).unsqueeze(0),
|
||||
size=shape,
|
||||
size=tuple(shape_list),
|
||||
mode=self.interpolation_mode,
|
||||
)
|
||||
.squeeze(0)
|
||||
.permute((1, 2, 0))
|
||||
.flatten(end_dim=1)
|
||||
)
|
||||
out = x + torch.cat(pos_embs)
|
||||
if not pos_embs:
|
||||
return self.weight.new_zeros((0, self.weight.shape[-1]))
|
||||
return torch.cat(pos_embs)
|
||||
|
||||
def forward(self, x: torch.Tensor, grid_hws: torch.Tensor) -> torch.Tensor:
|
||||
pos_embs = self.get_pos_embeds(grid_hws.tolist())
|
||||
out = x + pos_embs
|
||||
return out
|
||||
|
||||
|
||||
@@ -158,19 +179,29 @@ class MoonVisionPatchEmbed(nn.Module):
|
||||
height=pos_emb_height, width=pos_emb_width, dim=out_dim
|
||||
)
|
||||
|
||||
def forward(self, x: torch.Tensor, grid_hw: torch.Tensor) -> torch.Tensor:
|
||||
def forward(
|
||||
self,
|
||||
x: torch.Tensor,
|
||||
grid_hw: torch.Tensor | None = None,
|
||||
*,
|
||||
pos_embeds: torch.Tensor | None = None,
|
||||
) -> torch.Tensor:
|
||||
"""
|
||||
Args:
|
||||
x (L, Channels): input tensor
|
||||
grid_hw (N, 2): grid height and width
|
||||
pos_embeds: precomputed positional embeddings of shape
|
||||
``(L, Cout)``. When provided, ``grid_hw`` is unused and the
|
||||
CUDA-graph-incompatible interpolation in ``self.pos_emb`` is
|
||||
skipped.
|
||||
|
||||
Returns:
|
||||
(L, Cout) tensor
|
||||
"""
|
||||
x = self.proj(x).view(x.size(0), -1)
|
||||
# apply positional embedding
|
||||
x = self.pos_emb(x, grid_hw)
|
||||
return x
|
||||
if pos_embeds is not None:
|
||||
return x + pos_embeds
|
||||
return self.pos_emb(x, grid_hw)
|
||||
|
||||
|
||||
class Rope2DPosEmb(nn.Module):
|
||||
@@ -243,6 +274,35 @@ class Rope2DPosEmb(nn.Module):
|
||||
freqs_cis = freqs_cis.reshape(self.max_height, self.max_width, -1)
|
||||
return freqs_cis
|
||||
|
||||
def get_freqs_cis_by_seqlens_list(
|
||||
self,
|
||||
grid_hws_list: list[list[int]] | list[tuple[int, int]],
|
||||
) -> torch.Tensor:
|
||||
"""List-based variant of :meth:`get_freqs_cis_by_seqlens`.
|
||||
|
||||
Accepts a Python list of ``(h, w)`` pairs so callers that already
|
||||
operate outside the captured CUDA graph can avoid materializing a
|
||||
tensor + ``.tolist()`` round-trip.
|
||||
"""
|
||||
assert all(
|
||||
1 <= h <= self.max_height and 1 <= w <= self.max_width
|
||||
for h, w in grid_hws_list
|
||||
), (
|
||||
grid_hws_list,
|
||||
self.max_height,
|
||||
self.max_width,
|
||||
)
|
||||
if not grid_hws_list:
|
||||
return self.precomputed_freqs_cis.new_zeros((0, self.dim // 2))
|
||||
freqs_cis = torch.cat(
|
||||
[
|
||||
self.precomputed_freqs_cis[:h, :w].reshape(-1, self.dim // 2)
|
||||
for h, w in grid_hws_list
|
||||
],
|
||||
dim=0,
|
||||
)
|
||||
return freqs_cis
|
||||
|
||||
def get_freqs_cis_by_seqlens(self, grid_hws: torch.Tensor) -> torch.Tensor:
|
||||
"""
|
||||
Args:
|
||||
@@ -250,22 +310,7 @@ class Rope2DPosEmb(nn.Module):
|
||||
Returns:
|
||||
freqs_cis: tensor of shape (sum(t * height * width), dim//2)
|
||||
"""
|
||||
shapes = grid_hws.tolist()
|
||||
assert all(
|
||||
1 <= h <= self.max_height and 1 <= w <= self.max_width for h, w in shapes
|
||||
), (
|
||||
shapes,
|
||||
self.max_height,
|
||||
self.max_width,
|
||||
)
|
||||
freqs_cis = torch.cat(
|
||||
[
|
||||
self.precomputed_freqs_cis[:h, :w].reshape(-1, self.dim // 2)
|
||||
for h, w in shapes
|
||||
],
|
||||
dim=0,
|
||||
)
|
||||
return freqs_cis
|
||||
return self.get_freqs_cis_by_seqlens_list(grid_hws.tolist())
|
||||
|
||||
def get_freqs_cis_by_idx(
|
||||
self, pos_idx: torch.Tensor, pos_idx_mask: torch.Tensor
|
||||
@@ -392,11 +437,15 @@ class MoonVitEncoderLayer(nn.Module):
|
||||
x: torch.Tensor,
|
||||
cu_seqlens: torch.Tensor,
|
||||
rope_freqs_cis: torch.Tensor | None = None,
|
||||
max_seqlen: torch.Tensor | None = None,
|
||||
):
|
||||
"""
|
||||
Args:
|
||||
x (torch.Tensor): (seqlen, hidden_dim)
|
||||
cu_seqlens (torch.Tensor):
|
||||
max_seqlen: Optional precomputed scalar tensor. When omitted it
|
||||
is derived from ``cu_seqlens``, which produces a GPU scalar
|
||||
that breaks CUDA graph capture.
|
||||
"""
|
||||
seq_length = x.size(0)
|
||||
xqkv, _ = self.wqkv(x)
|
||||
@@ -412,7 +461,8 @@ class MoonVitEncoderLayer(nn.Module):
|
||||
|
||||
xq, xk = apply_rope(xq, xk, rope_freqs_cis)
|
||||
|
||||
max_seqlen = (cu_seqlens[1:] - cu_seqlens[:-1]).max()
|
||||
if max_seqlen is None:
|
||||
max_seqlen = (cu_seqlens[1:] - cu_seqlens[:-1]).max()
|
||||
attn_out = self.attn(
|
||||
xq.unsqueeze(0),
|
||||
xk.unsqueeze(0),
|
||||
@@ -433,10 +483,12 @@ class MoonVitEncoderLayer(nn.Module):
|
||||
hidden_states: torch.Tensor,
|
||||
cu_seqlens: torch.Tensor,
|
||||
rope_freqs_cis: torch.Tensor | None = None,
|
||||
max_seqlen: torch.Tensor | None = None,
|
||||
) -> torch.Tensor:
|
||||
"""
|
||||
Args:
|
||||
hidden_states: non-packed (B, N, D) or packed (L, D). if non-packed, seqlens should be None, if packed, seqlens should be set
|
||||
max_seqlen: optional precomputed max-sequence-length scalar.
|
||||
|
||||
Returns:
|
||||
output: same shape of input, non-packed (B, N, D) for non-packed input, (L, D) for packed input
|
||||
@@ -444,7 +496,10 @@ class MoonVitEncoderLayer(nn.Module):
|
||||
residual = hidden_states
|
||||
hidden_states = self.norm0(hidden_states)
|
||||
attn_out = self.attention_qkvpacked(
|
||||
hidden_states, cu_seqlens, rope_freqs_cis=rope_freqs_cis
|
||||
hidden_states,
|
||||
cu_seqlens,
|
||||
rope_freqs_cis=rope_freqs_cis,
|
||||
max_seqlen=max_seqlen,
|
||||
)
|
||||
hidden_states = residual + attn_out
|
||||
|
||||
@@ -478,22 +533,39 @@ class MoonVitEncoder(nn.Module):
|
||||
)
|
||||
self.final_layernorm = nn.LayerNorm(hidden_dim)
|
||||
|
||||
def forward(
|
||||
self, hidden_states: torch.Tensor, grid_hw: torch.Tensor
|
||||
def get_rope_freqs_cis(
|
||||
self,
|
||||
grid_hws_list: list[list[int]] | list[tuple[int, int]],
|
||||
) -> torch.Tensor:
|
||||
rope_freqs_cis = self.rope_2d.get_freqs_cis_by_seqlens(grid_hws=grid_hw)
|
||||
return self.rope_2d.get_freqs_cis_by_seqlens_list(grid_hws_list)
|
||||
|
||||
lengths = torch.cat(
|
||||
(
|
||||
torch.zeros(1, device=hidden_states.device, dtype=grid_hw.dtype),
|
||||
(grid_hw[:, 0] * grid_hw[:, 1]).to(hidden_states.device),
|
||||
def forward(
|
||||
self,
|
||||
hidden_states: torch.Tensor,
|
||||
grid_hw: torch.Tensor | None = None,
|
||||
*,
|
||||
cu_seqlens: torch.Tensor | None = None,
|
||||
rope_freqs_cis: torch.Tensor | None = None,
|
||||
max_seqlen: torch.Tensor | None = None,
|
||||
) -> torch.Tensor:
|
||||
if rope_freqs_cis is None:
|
||||
rope_freqs_cis = self.rope_2d.get_freqs_cis_by_seqlens(grid_hws=grid_hw)
|
||||
|
||||
if cu_seqlens is None:
|
||||
lengths = torch.cat(
|
||||
(
|
||||
torch.zeros(1, device=hidden_states.device, dtype=grid_hw.dtype),
|
||||
(grid_hw[:, 0] * grid_hw[:, 1]).to(hidden_states.device),
|
||||
)
|
||||
)
|
||||
)
|
||||
cu_seqlens = lengths.cumsum(dim=0, dtype=torch.int32)
|
||||
cu_seqlens = lengths.cumsum(dim=0, dtype=torch.int32)
|
||||
|
||||
for _, block in enumerate(self.blocks):
|
||||
hidden_states = block(
|
||||
hidden_states, cu_seqlens, rope_freqs_cis=rope_freqs_cis
|
||||
hidden_states,
|
||||
cu_seqlens,
|
||||
rope_freqs_cis=rope_freqs_cis,
|
||||
max_seqlen=max_seqlen,
|
||||
)
|
||||
|
||||
hidden_states = self.final_layernorm(hidden_states)
|
||||
@@ -530,6 +602,54 @@ def patch_merger(
|
||||
return outputs
|
||||
|
||||
|
||||
def patch_merger_packed(
|
||||
x: torch.Tensor,
|
||||
gather_idx: torch.Tensor,
|
||||
merge_kernel_size: tuple[int, int],
|
||||
) -> torch.Tensor:
|
||||
"""CUDA-graph-safe equivalent of :func:`patch_merger`.
|
||||
|
||||
Uses a precomputed index tensor to gather the per-token reshape +
|
||||
permute that ``patch_merger`` does inside a Python loop. The output is
|
||||
the concatenated 3D tensor ``(sum(new_h * new_w), kh * kw, d_model)``,
|
||||
matching what ``torch.cat(patch_merger(...))`` would produce.
|
||||
"""
|
||||
kh, kw = merge_kernel_size
|
||||
d_model = x.size(-1)
|
||||
return x.index_select(0, gather_idx).view(-1, kh * kw, d_model)
|
||||
|
||||
|
||||
def _build_merge_gather_idx(
|
||||
grid_hws_list: list[list[int]] | list[tuple[int, int]],
|
||||
merge_kernel_size: tuple[int, int],
|
||||
) -> np.ndarray:
|
||||
"""Build the per-token gather indices used by :func:`patch_merger_packed`.
|
||||
|
||||
For each item with grid (h, w) and merge kernel (kh, kw), the output
|
||||
block at position (nh, nw) gathers the kh*kw input tokens at rows
|
||||
(nh*kh + ih, nw*kw + iw) of that item, in (ih, iw) row-major order.
|
||||
"""
|
||||
kh, kw = merge_kernel_size
|
||||
parts: list[np.ndarray] = []
|
||||
pre_sum = 0
|
||||
for h, w in grid_hws_list:
|
||||
new_h, new_w = h // kh, w // kw
|
||||
nh = np.arange(new_h, dtype=np.int64).reshape(new_h, 1, 1, 1)
|
||||
nw = np.arange(new_w, dtype=np.int64).reshape(1, new_w, 1, 1)
|
||||
ih = np.arange(kh, dtype=np.int64).reshape(1, 1, kh, 1)
|
||||
iw = np.arange(kw, dtype=np.int64).reshape(1, 1, 1, kw)
|
||||
# Linearized input row = (nh*kh + ih) * w + (nw*kw + iw), offset by
|
||||
# the per-item base ``pre_sum``. Output is laid out as
|
||||
# (new_h, new_w, kh, kw) which patch_merger flattens to
|
||||
# (new_h*new_w, kh*kw).
|
||||
idx = pre_sum + (nh * kh + ih) * w + (nw * kw + iw)
|
||||
parts.append(idx.reshape(-1))
|
||||
pre_sum += h * w
|
||||
if not parts:
|
||||
return np.zeros(0, dtype=np.int64)
|
||||
return np.concatenate(parts)
|
||||
|
||||
|
||||
class MoonVitPretrainedModel(PreTrainedModel):
|
||||
config_class = MoonViTConfig
|
||||
model_type = "moonvit"
|
||||
@@ -570,17 +690,126 @@ class MoonVitPretrainedModel(PreTrainedModel):
|
||||
prefix=f"{prefix}.encoder",
|
||||
)
|
||||
|
||||
def prepare_encoder_metadata(
|
||||
self,
|
||||
grid_hws_list: list[list[int]] | list[tuple[int, int]],
|
||||
*,
|
||||
max_batch_size: int | None = None,
|
||||
max_seqlen_override: int | None = None,
|
||||
device: torch.device | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Precompute every grid-dependent input the encoder needs.
|
||||
|
||||
Used by the CUDA graph capture and replay paths to precompute
|
||||
every grid-dependent input outside the captured graph, so per-grid
|
||||
Python iteration and ``.tolist()`` round-trips are fine; the
|
||||
values are then copied into fixed-shape buffers for replay.
|
||||
|
||||
Args:
|
||||
grid_hws_list: List of ``(h, w)`` patch-grid sizes per image.
|
||||
max_batch_size: When set, ``cu_seqlens`` is right-padded with
|
||||
its last value so the buffer covers up to this many
|
||||
sequences. Required at CUDA graph capture/replay so the
|
||||
buffer shape matches what was recorded; padding entries
|
||||
are zero-length sequences and are ignored by varlen
|
||||
attention.
|
||||
max_seqlen_override: Override the per-replay max sequence
|
||||
length scalar. At capture this must be a safe upper bound
|
||||
(worst case: a single image consuming the full token
|
||||
budget) because the value is baked into the captured
|
||||
graph.
|
||||
device: Device for the metadata tensors. Defaults to the
|
||||
model's parameter device.
|
||||
"""
|
||||
if device is None:
|
||||
device = next(self.parameters()).device
|
||||
|
||||
# Normalize to a list of plain Python int pairs so the helpers
|
||||
# below never need ``.tolist()`` on a tensor.
|
||||
grid_pairs: list[tuple[int, int]] = [(int(h), int(w)) for h, w in grid_hws_list]
|
||||
|
||||
metadata: dict[str, Any] = {}
|
||||
|
||||
pos_embeds = self.patch_embed.pos_emb.get_pos_embeds(grid_pairs)
|
||||
metadata["pos_embeds"] = pos_embeds.to(device=device)
|
||||
|
||||
rope_freqs_cis = self.encoder.get_rope_freqs_cis(grid_pairs)
|
||||
metadata["rope_freqs_cis"] = rope_freqs_cis.to(device=device)
|
||||
|
||||
grid_arr = np.array(grid_pairs, dtype=np.int64)
|
||||
seq_lens = (grid_arr[:, 0] * grid_arr[:, 1]).astype(np.int32)
|
||||
cu_seqlens_np = np.concatenate(
|
||||
[
|
||||
np.zeros(1, dtype=np.int32),
|
||||
seq_lens.cumsum(dtype=np.int32),
|
||||
]
|
||||
)
|
||||
|
||||
if max_batch_size is not None:
|
||||
num_seqs = len(cu_seqlens_np) - 1
|
||||
if num_seqs < max_batch_size:
|
||||
cu_seqlens_np = np.concatenate(
|
||||
[
|
||||
cu_seqlens_np,
|
||||
np.full(
|
||||
max_batch_size - num_seqs,
|
||||
cu_seqlens_np[-1],
|
||||
dtype=np.int32,
|
||||
),
|
||||
]
|
||||
)
|
||||
metadata["cu_seqlens"] = torch.from_numpy(cu_seqlens_np).to(device)
|
||||
|
||||
if max_seqlen_override is not None:
|
||||
max_seqlen_val = int(max_seqlen_override)
|
||||
else:
|
||||
max_seqlen_val = int(seq_lens.max()) if len(seq_lens) > 0 else 0
|
||||
# Keep on CPU: attention wrappers may call .item() on this scalar
|
||||
# and we want that materialization to happen outside the captured
|
||||
# graph (the value is constant per capture anyway).
|
||||
metadata["max_seqlen"] = torch.tensor(max_seqlen_val, dtype=torch.int32)
|
||||
|
||||
gather_idx_np = _build_merge_gather_idx(grid_pairs, self.merge_kernel_size)
|
||||
metadata["merge_gather_idx"] = torch.from_numpy(gather_idx_np).to(device)
|
||||
|
||||
return metadata
|
||||
|
||||
def forward(
|
||||
self, pixel_values: torch.Tensor, grid_hw: torch.Tensor
|
||||
) -> torch.Tensor:
|
||||
self,
|
||||
pixel_values: torch.Tensor,
|
||||
grid_hw: torch.Tensor,
|
||||
*,
|
||||
encoder_metadata: dict[str, Any] | None = None,
|
||||
) -> torch.Tensor | list[torch.Tensor]:
|
||||
"""
|
||||
Args:
|
||||
pixel_values (torch.Tensor): The input pixel values.
|
||||
grid_hw (torch.Tensor): The grid height and width.
|
||||
|
||||
Returns:
|
||||
torch.Tensor: The output tokens.
|
||||
encoder_metadata: Optional precomputed metadata produced by
|
||||
:meth:`prepare_encoder_metadata`. When provided every
|
||||
``.tolist()`` call in the forward path is skipped, the
|
||||
returned tensor is the packed
|
||||
``(sum(new_h*new_w), kh*kw, hidden_size)`` form (suitable
|
||||
for CUDA graph capture/replay), and ``grid_hw`` is unused.
|
||||
When ``None`` the legacy path runs and returns a list of
|
||||
per-image tensors.
|
||||
"""
|
||||
if encoder_metadata is not None:
|
||||
hidden_states = self.patch_embed(
|
||||
pixel_values, pos_embeds=encoder_metadata["pos_embeds"]
|
||||
)
|
||||
hidden_states = self.encoder(
|
||||
hidden_states,
|
||||
cu_seqlens=encoder_metadata["cu_seqlens"],
|
||||
rope_freqs_cis=encoder_metadata["rope_freqs_cis"],
|
||||
max_seqlen=encoder_metadata["max_seqlen"],
|
||||
)
|
||||
return patch_merger_packed(
|
||||
hidden_states,
|
||||
encoder_metadata["merge_gather_idx"],
|
||||
merge_kernel_size=self.merge_kernel_size,
|
||||
)
|
||||
|
||||
hidden_states = self.patch_embed(pixel_values, grid_hw)
|
||||
hidden_states = self.encoder(hidden_states, grid_hw)
|
||||
hidden_states = patch_merger(
|
||||
|
||||
Reference in New Issue
Block a user