Skip to content
9 changes: 9 additions & 0 deletions docs/design/cuda_graphs_multimodal.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ Models opt-in to encoder CUDA Graphs by implementing the [SupportsEncoderCudaGra

| Architecture | Models | CG for Image | CG for Video |
| ------------ | ------ | ------------ | ------------ |
| `Llama4ForConditionalGeneration` | `Llama 4` | ✅︎ | - |
| `InternVLChatModel` | `InternVL3.5`, `InternVL3`, `InternVL2.5`, `InternVL2` | ✅︎ | ✅︎ |
| `Qwen2VLForConditionalGeneration` | `Qwen2-VL` | ✅︎ | ✅︎ |
| `Qwen2_5_VLForConditionalGeneration` | `Qwen2.5-VL` | ✅︎ | ✅︎ |
Expand Down Expand Up @@ -113,6 +114,14 @@ vllm serve Qwen/Qwen3-VL-32B \
--compilation-config '{"cudagraph_mm_encoder": true}'
```

For `Llama 4` (image only):

```bash
vllm serve meta-llama/Llama-4-Scout-17B-16E-Instruct \
--limit-mm-per-prompt '{"image": 1}' \
--compilation-config '{"cudagraph_mm_encoder": true}'
```

With explicit budgets:

```bash
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 @@ -2554,6 +2554,7 @@ def run_tarsier2(questions: list[str], modality: str) -> ModelRequestData:


MODELS_SUPPORT_VIT_CUDA_GRAPH = [
"llama4",
"internvl_chat",
"qwen2_5_vl",
"qwen3_vl",
Expand Down
22 changes: 21 additions & 1 deletion tests/models/multimodal/generation/test_vit_cudagraph.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from vllm.platforms import current_platform

from ....conftest import IMAGE_ASSETS, VIDEO_ASSETS
from ...utils import dummy_hf_overrides
from ....utils import dummy_hf_overrides

Check failure on line 13 in tests/models/multimodal/generation/test_vit_cudagraph.py

View workflow job for this annotation

GitHub Actions / pre-commit

Module "tests.utils" has no attribute "dummy_hf_overrides" [attr-defined]

Check failure on line 13 in tests/models/multimodal/generation/test_vit_cudagraph.py

View workflow job for this annotation

GitHub Actions / pre-commit

Module "tests.utils" has no attribute "dummy_hf_overrides" [attr-defined]

Check failure on line 13 in tests/models/multimodal/generation/test_vit_cudagraph.py

View workflow job for this annotation

GitHub Actions / pre-commit

Module "tests.utils" has no attribute "dummy_hf_overrides" [attr-defined]

Check failure on line 13 in tests/models/multimodal/generation/test_vit_cudagraph.py

View workflow job for this annotation

GitHub Actions / pre-commit

Module "tests.utils" has no attribute "dummy_hf_overrides" [attr-defined]
from .vlm_utils.builders import sample_frames_with_video_metadata


Expand Down Expand Up @@ -55,6 +55,26 @@


MODEL_CONFIGS: dict[str, VitCudagraphTestConfig] = {
"llama4": VitCudagraphTestConfig(
model="meta-llama/Llama-4-Scout-17B-16E-Instruct",

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.

I'm not sure if this 17B model would cause OOM issues in the CI.

FYI, #43082.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

yea... this is tricky. llama4 cg test is probably too heavy. I can shrink the config more but it looks like the only options are moving the llama4 cg testing to a different hardware config or not testing llama4.

what r ur thoughts?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

You can use dummy weights to test this model, because we only test basic functionality here. You can refer to Step3-VL below to reduce num layers in config.

modalities=["image"],
image_prompt=(
"<|begin_of_text|><|header_start|>user<|header_end|>\n\n"
"<|image|>What is in this image?<|eot|>"
"<|header_start|>assistant<|header_end|>\n\n"
),
Comment thread
This conversation was marked as resolved.
max_model_len=4096,
max_tokens=32,
max_num_seqs=2,
vllm_runner_kwargs={
"load_format": "dummy",
"hf_overrides": partial(
dummy_hf_overrides,
model_arch="Llama4ForConditionalGeneration",
),
},
marks=[pytest.mark.core_model],
),
"internvl": VitCudagraphTestConfig(
model="OpenGVLab/InternVL3-1B",
num_video_frames=8,
Expand Down
172 changes: 160 additions & 12 deletions vllm/model_executor/models/mllama4.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
import math
from collections.abc import Iterable, Mapping
from itertools import tee
from typing import Annotated, Literal
from typing import Annotated, Any, Literal

import torch
from torch import nn
Expand Down Expand Up @@ -75,6 +75,7 @@
MixtureOfExperts,
MultiModalEmbeddings,
SupportsEagle3,
SupportsEncoderCudaGraph,
SupportsLoRA,
SupportsMultiModal,
SupportsPP,
Expand Down Expand Up @@ -102,7 +103,7 @@ class Llama4ImagePatchInputs(TensorSchema):

patches_per_image: Annotated[torch.Tensor, TensorShape("batch_size")]
"""
The number of total patches for each image in the batch.
The number of chunked image tiles for each image in the batch.

This is used to split the embeddings which has the first two dimensions
flattened just like `pixel_values`.
Expand Down Expand Up @@ -728,6 +729,7 @@ class Llama4ForConditionalGeneration(
SupportsMultiModal,
SupportsPP,
MixtureOfExperts,
SupportsEncoderCudaGraph,
SupportsEagle3,
SupportsLoRA,
):
Expand Down Expand Up @@ -825,10 +827,161 @@ def update_physical_experts_metadata(
num_physical_experts, num_local_physical_experts
)

def get_image_patches_per_chunk(self) -> int:
return Mllama4ProcessingInfo.get_patch_per_chunk(self.config.vision_config)

def encode_image_chunks(
self,
pixel_values: torch.Tensor,
*,
use_data_parallel: bool,
) -> torch.Tensor:
if use_data_parallel:
vision_embeddings = run_dp_sharded_vision_model(
pixel_values, self.vision_model
)
else:
vision_embeddings = self.vision_model(pixel_values)

return self.multi_modal_projector(vision_embeddings)

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

return EncoderCudaGraphConfig(
modalities=["image"],
buffer_keys=["pixel_values"],
out_hidden_size=self.config.text_config.hidden_size,
)

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

def get_encoder_cudagraph_budget_range(
self,
vllm_config: VllmConfig,
) -> tuple[int, int]:
min_budget = self.get_image_patches_per_chunk()
max_budget = min(
vllm_config.scheduler_config.max_num_batched_tokens,
self.vllm_config.model_config.max_model_len,
)
return (min_budget, max_budget)

def get_encoder_cudagraph_item_specs(
self,
mm_kwargs: dict[str, Any],
):
from vllm.v1.worker.encoder_cudagraph_defs import EncoderItemSpec

patches_per_chunk = self.get_image_patches_per_chunk()
return [
EncoderItemSpec(
input_size=num_chunks,
output_tokens=num_chunks * patches_per_chunk,
)
for num_chunks in mm_kwargs["patches_per_image"].tolist()
]

def select_encoder_cudagraph_items(
self,
mm_kwargs: dict[str, Any],
indices: list[int],
) -> dict[str, Any]:
pixel_values = mm_kwargs["pixel_values"]
patches_per_image = mm_kwargs["patches_per_image"]

if len(indices) == 0:
return {
"pixel_values": pixel_values[:0],
"patches_per_image": patches_per_image[:0],
}

cum_chunks = [0]
for num_chunks in patches_per_image.tolist():
cum_chunks.append(cum_chunks[-1] + num_chunks)

selected_pixel_values = torch.cat(
[pixel_values[cum_chunks[i] : cum_chunks[i + 1]] for i in indices],
dim=0,
)

return {
"pixel_values": selected_pixel_values,
"patches_per_image": patches_per_image[indices],
}

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,
)

vision_config = self.config.vision_config
patches_per_chunk = self.get_image_patches_per_chunk()
chunks_per_capture = max(
1, (token_budget + patches_per_chunk - 1) // patches_per_chunk
)
dummy_pixel_values = torch.randn(
chunks_per_capture,
vision_config.num_channels,
vision_config.image_size,
vision_config.image_size,
device=device,
dtype=dtype,
)
Comment thread
This conversation was marked as resolved.

return EncoderCudaGraphCaptureInputs(
values={"pixel_values": 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,
)

return EncoderCudaGraphReplayBuffers(
values={"pixel_values": mm_kwargs["pixel_values"]},
)

def encoder_cudagraph_forward(
self,
inputs: dict[str, torch.Tensor],
) -> torch.Tensor:
return self.encode_image_chunks(
inputs["pixel_values"],
use_data_parallel=False,
).flatten(0, 1)
Comment thread
This conversation was marked as resolved.

def encoder_eager_forward(
self,
mm_kwargs: dict[str, Any],
) -> torch.Tensor:
return self.encode_image_chunks(
mm_kwargs["pixel_values"],
use_data_parallel=False,
).flatten(0, 1)
Comment thread
This conversation was marked as resolved.

def _parse_and_validate_image_input(
self, **kwargs: object
) -> Llama4ImagePatchInputs | None:
# num_images, 1, num_chunks, channel, image_size, image_size
# total_num_chunks, channel, image_size, image_size
pixel_values = kwargs.pop("pixel_values", None)
if pixel_values is None:
return None
Expand All @@ -850,15 +1003,10 @@ def _process_image_input(
pixel_values = image_input["pixel_values"]
patches_per_image = image_input["patches_per_image"].tolist()

# shard image input
if self.use_data_parallel:
vision_embeddings_flat = run_dp_sharded_vision_model(
pixel_values, self.vision_model
)
else:
vision_embeddings_flat = self.vision_model(pixel_values)

vision_embeddings_flat = self.multi_modal_projector(vision_embeddings_flat)
vision_embeddings_flat = self.encode_image_chunks(
pixel_values,
use_data_parallel=self.use_data_parallel,
)

return [
img.flatten(0, 1)
Expand Down
Loading