forked from Karylab-cklius/vllm
[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:
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user