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 @@ -114,6 +115,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 @@ -2532,6 +2532,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
20 changes: 20 additions & 0 deletions tests/models/multimodal/generation/test_vit_cudagraph.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,26 @@ def step3_vl_chat_template(content: str) -> str:


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
5 changes: 3 additions & 2 deletions tests/models/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -506,12 +506,13 @@ class DummyConfig:
# Only set MoE related config when the model has MoE layers.
# Otherwise all models detected as MoE by _get_transformers_backend_cls.
if model_arch_config.num_experts > 0:
num_experts_per_tok = 1 if model_arch == "Llama4ForConditionalGeneration" else 2
update_dict.update(
{
"num_experts": num_experts,
"num_experts_per_tok": 2,
"num_experts_per_tok": num_experts_per_tok,
# Kimi uses `num_experts_per_token`.
"num_experts_per_token": 2,
"num_experts_per_token": num_experts_per_tok,
"num_local_experts": num_experts,
# Otherwise there will not be any expert layers
"first_k_dense_replace": 0,
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 @@ -78,6 +78,7 @@
MixtureOfExperts,
MultiModalEmbeddings,
SupportsEagle3,
SupportsEncoderCudaGraph,
SupportsLoRA,
SupportsMultiModal,
SupportsPP,
Expand Down Expand Up @@ -105,7 +106,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 @@ -731,6 +732,7 @@ class Llama4ForConditionalGeneration(
SupportsMultiModal,
SupportsPP,
MixtureOfExperts,
SupportsEncoderCudaGraph,
SupportsEagle3,
SupportsLoRA,
):
Expand Down Expand Up @@ -828,10 +830,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 @@ -853,15 +1006,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