[MM][Perf][CG] Support ViT full CUDA graph for InternVL (#41759)

Signed-off-by: oguz <oguzhankir17@gmail.com>
Co-authored-by: Isotr0py <mozf@mail2.sysu.edu.cn>
This commit is contained in:
Oğuzhan KIR
2026-06-04 10:24:25 +08:00
committed by GitHub
co-authored by Isotr0py
parent b58e082d95
commit f25952e59b
4 changed files with 183 additions and 2 deletions
+1
View File
@@ -82,6 +82,7 @@ Models opt-in to encoder CUDA Graphs by implementing the [SupportsEncoderCudaGra
| Architecture | Models | CG for Image | CG for Video |
| ------------ | ------ | ------------ | ------------ |
| `InternVLChatModel` | `InternVL3.5`, `InternVL3`, `InternVL2.5`, `InternVL2` | ✅︎ | ✅︎ |
| `Qwen2VLForConditionalGeneration` | `Qwen2-VL` | ✅︎ | ✅︎ |
| `Qwen2_5_VLForConditionalGeneration` | `Qwen2.5-VL` | ✅︎ | ✅︎ |
| `Qwen3VLForConditionalGeneration` | `Qwen3-VL` | ✅︎ | ✅︎ |
@@ -2554,6 +2554,7 @@ MODELS_NEED_VIDEO_METADATA = [
MODELS_SUPPORT_VIT_CUDA_GRAPH = [
"internvl_chat",
"qwen2_5_vl",
"qwen3_vl",
"qwen3_vl_moe",
@@ -43,6 +43,10 @@ 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"
def step3_vl_chat_template(content: str) -> str:
return (
"<begin▁of▁sentence> You are a helpful assistant.<|BOT|>user\n "
@@ -51,6 +55,17 @@ def step3_vl_chat_template(content: str) -> str:
MODEL_CONFIGS: dict[str, VitCudagraphTestConfig] = {
"internvl": VitCudagraphTestConfig(
model="OpenGVLab/InternVL3-1B",
num_video_frames=8,
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],
),
"qwen2_5_vl": VitCudagraphTestConfig(
model="Qwen/Qwen2.5-VL-3B-Instruct",
image_prompt=qwen_vl_chat_template(
+166 -2
View File
@@ -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, Literal, TypeAlias, TypeVar
import torch
import torch.nn as nn
@@ -55,6 +55,7 @@ from vllm.utils.tensor_schema import TensorSchema, TensorShape
from .interfaces import (
MultiModalEmbeddings,
SupportsEncoderCudaGraph,
SupportsLoRA,
SupportsMultiModal,
SupportsPP,
@@ -543,7 +544,13 @@ class InternVLMultiModalProcessor(
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
@classmethod
@@ -924,3 +931,160 @@ class InternVLChatModel(nn.Module, SupportsMultiModal, SupportsPP, SupportsLoRA)
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"],
# InternVision uses standard ViT attention (no rotary embeddings,
# no variable-length sequence metadata), so the only graph-recorded
# buffer is pixel_values_flat itself.
buffer_keys=["pixel_values_flat"],
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_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_item_specs(
self,
mm_kwargs: dict[str, Any],
):
from vllm.v1.worker.encoder_cudagraph_defs import EncoderItemSpec
return [
EncoderItemSpec(
input_size=n,
output_tokens=n * self.num_image_token,
)
for n in 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
)
return EncoderCudaGraphCaptureInputs(
values={"pixel_values_flat": dummy_pixel_values},
)
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,
)
modality = self.get_input_modality(mm_kwargs)
pv_key = (
"pixel_values_flat" if modality == "image" else "pixel_values_flat_video"
)
return EncoderCudaGraphReplayBuffers(
values={"pixel_values_flat": mm_kwargs[pv_key]},
)
def encoder_cudagraph_forward(
self,
values: 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 = values["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)