Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/design/cuda_graphs_multimodal.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ Models opt-in to encoder CUDA Graphs by implementing the [SupportsEncoderCudaGra

| Architecture | Models | CG for Image | CG for Video |
| ------------ | ------ | ------------ | ------------ |
| `InternVLChatModel` | `InternVL3`, `InternVL2.5`, `InternVL2` | ✅︎ | ✅︎ |

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this PR support InternVL3_5?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. InternVL3.5 resolves to the same InternVLChatModel, so it's covered. Added it to the docs table.

| `Qwen3VLForConditionalGeneration` | `Qwen3-VL` | ✅︎ | ✅︎ |
| `Qwen2_5_VLForConditionalGeneration` | `Qwen2.5-VL` | ✅︎ | ✅︎ |

Expand Down
1 change: 1 addition & 0 deletions examples/generate/multimodal/vision_language_offline.py
Original file line number Diff line number Diff line change
Expand Up @@ -2464,6 +2464,7 @@ def run_tarsier2(questions: list[str], modality: str) -> ModelRequestData:


MODELS_SUPPORT_VIT_CUDA_GRAPH = [
"internvl_chat",
"qwen3_vl",
"qwen3_vl_moe",
"qwen2_5_vl",
Expand Down
14 changes: 14 additions & 0 deletions tests/models/multimodal/generation/test_vit_cudagraph.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,21 @@ def qwen_vl_chat_template(content: str) -> str:
return f"<|im_start|>user\n{content}<|im_end|>\n<|im_start|>assistant\n"


def internvl_chat_template(content: str) -> str:
return f"<|im_start|>user\n{content}<|im_end|>\n<|im_start|>assistant\n"


MODEL_CONFIGS: dict[str, VitCudagraphTestConfig] = {
"internvl": VitCudagraphTestConfig(
model="OpenGVLab/InternVL3-2B",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For CI test, we should use a model as small as possible, I suppose OpenGVLab/InternVL3-1B is better.

image_prompt=internvl_chat_template("<image>\nWhat is in this image?"),
video_prompt=internvl_chat_template(
"<video>\nDescribe this video in one sentence."
),
needs_video_metadata=False,
vllm_runner_kwargs={"trust_remote_code": True},
marks=[pytest.mark.core_model],
),
"qwen3_vl": VitCudagraphTestConfig(
model="Qwen/Qwen3-VL-2B-Instruct",
image_prompt=qwen_vl_chat_template(
Expand Down
180 changes: 178 additions & 2 deletions vllm/model_executor/models/internvl.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from abc import abstractmethod
from collections.abc import Iterable, Mapping, Sequence
from functools import cached_property
from typing import Annotated, Literal, TypeAlias, TypeVar
from typing import Annotated, Any, ClassVar, Literal, TypeAlias, TypeVar

import torch
import torch.nn as nn
Expand Down Expand Up @@ -55,6 +55,7 @@

from .interfaces import (
MultiModalEmbeddings,
SupportsEncoderCudaGraph,
SupportsLoRA,
SupportsMultiModal,
SupportsPP,
Expand Down Expand Up @@ -541,8 +542,15 @@ def _get_prompt_updates(
info=InternVLProcessingInfo,
dummy_inputs=InternVLDummyInputsBuilder,
)
class InternVLChatModel(nn.Module, SupportsMultiModal, SupportsPP, SupportsLoRA):
class InternVLChatModel(
nn.Module,
SupportsMultiModal,
SupportsPP,
SupportsLoRA,
SupportsEncoderCudaGraph,
):
supports_encoder_tp_data = True
supports_encoder_cudagraph: ClassVar[Literal[True]] = True

@shen-shanshan shen-shanshan May 26, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This supports_encoder_cudagraph is not needed.


@classmethod
def get_placeholder_str(cls, modality: str, i: int) -> str | None:
Expand Down Expand Up @@ -922,3 +930,171 @@ def get_num_mm_connector_tokens(self, num_vision_tokens: int) -> int:

num_patches = num_vision_tokens // (self.patch_tokens + 1)
return num_patches * self.num_image_token

# -- SupportsEncoderCudaGraph protocol methods --

def get_encoder_cudagraph_config(self):
from vllm.v1.worker.encoder_cudagraph_defs import EncoderCudaGraphConfig

return EncoderCudaGraphConfig(
modalities=["image", "video"],
input_key_by_modality={
"image": "pixel_values_flat",
"video": "pixel_values_flat_video",
},
# InternVision uses standard ViT attention (no rotary embeddings,
# no variable-length sequence metadata), so no extra buffers needed.
buffer_keys=[],
out_hidden_size=self.config.text_config.hidden_size,
)

def get_input_modality(
self,
mm_kwargs: dict[str, Any],
) -> str:
if "pixel_values_flat" in mm_kwargs:
return "image"
return "video"

def get_max_frames_per_video(self) -> int:
# InternVL has no attention-metadata buffers that depend on frame
# count (buffer_keys=[]), so any value is safe. Return 1.
return 1

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


def get_encoder_cudagraph_budget_range(
self,
vllm_config: "VllmConfig",
) -> tuple[int, int]:
# Min: 1 tile → num_image_token output tokens.
min_budget = self.num_image_token
max_budget = min(
vllm_config.scheduler_config.max_num_batched_tokens,
vllm_config.model_config.max_model_len,
)
return (min_budget, max_budget)

def _get_internvl_patches_list(
self,
mm_kwargs: dict[str, Any],
) -> list[int]:
"""Return per-item tile counts as a plain list of ints."""
if self.get_input_modality(mm_kwargs) == "image":
patches = mm_kwargs.get("image_num_patches", [])
else:
patches = mm_kwargs.get("video_num_patches", [])
if isinstance(patches, torch.Tensor):
return patches.tolist()
return [int(n) for n in patches]

def get_encoder_cudagraph_num_items(
self,
mm_kwargs: dict[str, Any],
) -> int:
return len(self._get_internvl_patches_list(mm_kwargs))

def get_encoder_cudagraph_per_item_output_tokens(
self,
mm_kwargs: dict[str, Any],
) -> list[int]:
return [n * self.num_image_token
for n in self._get_internvl_patches_list(mm_kwargs)]

def get_encoder_cudagraph_per_item_input_sizes(
self,
mm_kwargs: dict[str, Any],
) -> list[int]:
return self._get_internvl_patches_list(mm_kwargs)

def select_encoder_cudagraph_items(
self,
mm_kwargs: dict[str, Any],
indices: list[int],
) -> dict[str, Any]:
modality = self.get_input_modality(mm_kwargs)
pv_key = ("pixel_values_flat"
if modality == "image" else "pixel_values_flat_video")
patches_key = ("image_num_patches"
if modality == "image" else "video_num_patches")

pixel_values = mm_kwargs[pv_key]
patches_list = self._get_internvl_patches_list(mm_kwargs)

if len(indices) == 0:
return {pv_key: pixel_values[:0], patches_key: []}

# Compute cumulative tile offsets for slicing pixel_values.
cum_patches = [0]
for n in patches_list:
cum_patches.append(cum_patches[-1] + n)

selected_pv = torch.cat(
[pixel_values[cum_patches[i]: cum_patches[i + 1]] for i in indices]
)
selected_patches = [patches_list[i] for i in indices]

return {pv_key: selected_pv, patches_key: selected_patches}

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,
):
from vllm.v1.worker.encoder_cudagraph_defs import (
EncoderCudaGraphCaptureInputs,
)

# Size the buffer to hold the maximum possible tiles for this budget.
total_tiles = max(token_budget // self.num_image_token, 1)
image_size = self.config.vision_config.image_size

dummy_pixel_values = torch.randn(
total_tiles, 3, image_size, image_size, device=device, dtype=dtype
)
mm_kwargs = {
"pixel_values_flat": dummy_pixel_values,
# Single dummy item consuming all tiles; not used inside
# extract_feature, only needed for structural consistency.
"image_num_patches": [total_tiles],
}

return EncoderCudaGraphCaptureInputs(mm_kwargs=mm_kwargs, buffers={})

def prepare_encoder_cudagraph_replay_buffers(
self,
mm_kwargs: dict[str, Any],
max_batch_size: int,
max_frames_per_batch: int,
):
from vllm.v1.worker.encoder_cudagraph_defs import (
EncoderCudaGraphReplayBuffers,
)

# No metadata buffers required for InternVision.
return EncoderCudaGraphReplayBuffers(buffers={})

def encoder_cudagraph_forward(
self,
mm_kwargs: dict[str, Any],
buffers: dict[str, torch.Tensor],
) -> torch.Tensor:
# The graph is always captured with pixel_values_flat as the input
# buffer. During video replay the manager copies video tiles into
# this same buffer before calling graph.replay(), so we always read
# from pixel_values_flat here.
pixel_values = mm_kwargs["pixel_values_flat"]
out = self.extract_feature(pixel_values) # [N, num_image_token, H]
return out.view(-1, self.config.text_config.hidden_size)

def encoder_eager_forward(
self,
mm_kwargs: dict[str, Any],
) -> torch.Tensor:
if self.get_input_modality(mm_kwargs) == "image":
pixel_values = mm_kwargs["pixel_values_flat"]
else:
pixel_values = mm_kwargs["pixel_values_flat_video"]
out = self.extract_feature(pixel_values) # [N, num_image_token, H]
return out.view(-1, self.config.text_config.hidden_size)
Loading