[MM][CG] Enable encoder CUDA Graph for MiniCPM-V (#42785)
Signed-off-by: YunzhuLu <lucia.yunzhu@gmail.com> Signed-off-by: Isotr0py <Isotr0py@outlook.com> Co-authored-by: Isotr0py <Isotr0py@outlook.com>
Yunzhu Lu committed
Sep 10, 2026 at 14:28 UTC
442d36031ce710aa2a777c353becb660c57ab2bd
26 files changed
+843
-103
docs/design/cuda_graphs_multimodal.md
+3
@@ -35,6 +35,8 @@ For two-tower vision encoders (e.g., DeepSeek-OCR's SAM + CLIP with dynamic tili
35
| `Qwen3_5ForConditionalGeneration` | `Qwen3.5`, `Qwen3.6` | ✅︎ | ✅︎ | ❌︎ |
36
| `Qwen3_5MoeForConditionalGeneration` | `Qwen3.5-MoE`, `Qwen3.6-MoE` | ✅︎ | ✅︎ | ❌︎ |
37
| `Step3VLForConditionalGeneration` | `Step3-VL` | ✅︎ | ❌︎ | ✅︎ |
38
+| `MiniCPMV` | `MiniCPMV2.5` | ✅︎ | ❌︎ | ❌︎ |
39
+| `MiniCPMV` | `MiniCPMV2.6`, `MiniCPMV4.0` | ✅︎ | ✅︎ | ❌︎ |
40
41
### Model x Hardware
42
@@ -54,6 +56,7 @@ For two-tower vision encoders (e.g., DeepSeek-OCR's SAM + CLIP with dynamic tili
56
| `Qwen3_5ForConditionalGeneration` | ✅︎ | ✅︎ | ❔ | ✅︎ |
57
| `Qwen3_5MoeForConditionalGeneration` | ✅︎ | ✅︎ | ❔ | ✅︎ |
58
| `Step3VLForConditionalGeneration` | ✅︎ | ✅︎ | ❔ | ✅︎ |
59
+| `MiniCPMV` | ✅︎ | ✅︎ | ❔ | ❔ |
60
61
!!! note
62
Encoder CUDA Graph has currently been tested with `--mm-encoder-attn-backend=FLASH_ATTN` and `--mm-encoder-attn-backend=FLASHINFER` on Blackwell GPUs.
examples/generate/multimodal/vision_language_offline.py
+3
@@ -2276,6 +2276,9 @@ MODELS_SUPPORT_VIT_CUDA_GRAPH = [
2276
"glm4_1v",
2277
"deepseek_ocr",
2278
"ernie45_vl",
2279
+ "minicpmv2_5_vl",
2280
+ "minicpmv2_6_vl",
2281
+ "minicpmv4_vl",
2282
]
2283
2284
tests/models/multimodal/generation/test_vit_cudagraph.py
+65
@@ -74,6 +74,20 @@ def ernie45_vl_chat_template(content: str) -> str:
74
)
75
76
77
+def minicpmv_25_chat_template(content: str) -> str:
78
+ """Llama3-style chat template used by MiniCPM-V 2.5."""
79
+ return (
80
+ f"<|begin_of_text|><|start_header_id|>user<|end_header_id|>\n\n"
81
+ f"{content}"
82
+ f"<|eot_id|><|start_header_id|>assistant<|end_header_id|>\n\n"
83
+ )
84
+
85
+
86
+def minicpmv_chat_template(content: str) -> str:
87
+ """ChatML template used by MiniCPM-V 2.6 / 4.0 / 4.5."""
88
+ return f"<|im_start|>user\n{content}<|im_end|>\n<|im_start|>assistant\n"
89
+
90
+
91
MODEL_CONFIGS: dict[str, VitCudagraphTestConfig] = {
92
"gemma3": VitCudagraphTestConfig(
93
model="google/gemma-3-4b-it",
@@ -316,6 +330,57 @@ MODEL_CONFIGS: dict[str, VitCudagraphTestConfig] = {
330
needs_video_metadata=True,
331
marks=[pytest.mark.core_model],
332
),
333
+ "minicpmv_25": VitCudagraphTestConfig(
334
+ model="openbmb/MiniCPM-Llama3-V-2_5",
335
+ modalities=["image"],
336
+ image_prompt=minicpmv_25_chat_template(
337
+ "(<image>./</image>)\nWhat is in this image?"
338
+ ),
339
+ # CI runs on 35GB MIG slices: every budget captures one graph per
340
+ # patch-grid bucket, so the default budgets through max_model_len
341
+ # OOM there. A small budget set covers the test images (each item
342
+ # is at most (max_slice_num + 1) * query_num = 640 tokens).
343
+ compilation_config_overrides={
344
+ "encoder_cudagraph_token_budgets": [64, 1024],
345
+ },
346
+ vllm_runner_kwargs={"trust_remote_code": True},
347
+ marks=[pytest.mark.core_model],
348
+ ),
349
+ "minicpmv_26": VitCudagraphTestConfig(
350
+ model="openbmb/MiniCPM-V-2_6",
351
+ image_prompt=minicpmv_chat_template(
352
+ "(<image>./</image>)\nWhat is in this image?"
353
+ ),
354
+ video_prompt=minicpmv_chat_template(
355
+ "(<video>./</video>)\nDescribe this video in one sentence."
356
+ ),
357
+ max_model_len=2048,
358
+ # Fewer frames keep the video item within the 1024 budget.
359
+ num_video_frames=4,
360
+ compilation_config_overrides={
361
+ "encoder_cudagraph_token_budgets": [64, 1024],
362
+ "encoder_cudagraph_max_frames_per_batch": 4,
363
+ },
364
+ vllm_runner_kwargs={"trust_remote_code": True},
365
+ marks=[pytest.mark.core_model],
366
+ ),
367
+ "minicpmv_40": VitCudagraphTestConfig(
368
+ model="openbmb/MiniCPM-V-4",
369
+ image_prompt=minicpmv_chat_template(
370
+ "(<image>./</image>)\nWhat is in this image?"
371
+ ),
372
+ video_prompt=minicpmv_chat_template(
373
+ "(<video>./</video>)\nDescribe this video in one sentence."
374
+ ),
375
+ max_model_len=2048,
376
+ num_video_frames=4,
377
+ compilation_config_overrides={
378
+ "encoder_cudagraph_token_budgets": [64, 1024],
379
+ "encoder_cudagraph_max_frames_per_batch": 4,
380
+ },
381
+ vllm_runner_kwargs={"trust_remote_code": True},
382
+ marks=[pytest.mark.core_model],
383
+ ),
384
}
385
386
tests/v1/cudagraph/test_encoder_cudagraph.py
+10
@@ -12,6 +12,7 @@ Test organization:
12
- TestEncoderCudaGraphVideoReplay — video modality capture, replay
13
"""
14
15
+from collections.abc import Hashable
16
from typing import Any
17
18
import pytest
@@ -116,9 +117,15 @@ def _make_manager_with_budgets(budgets: list[int]) -> EncoderCudaGraphManager:
117
)
118
mgr.budget_graphs = {"default": {}}
119
mgr.graph_pool = None
120
+ mgr._capture_axes = ()
121
mgr.graph_hits = 0
122
mgr.graph_misses = 0
123
mgr.log_stats_interval = 100
124
+ mgr.config = EncoderCudaGraphConfig(
125
+ modalities=["image"],
126
+ buffer_keys=[],
127
+ out_hidden_size=32,
128
+ )
129
return mgr
130
131
@@ -348,6 +355,7 @@ class SimpleMockViTModel(torch.nn.Module, SupportsEncoderCudaGraph):
355
device: torch.device,
356
dtype: torch.dtype,
357
path: str = "default",
358
+ axis_keys: tuple[Hashable, ...] | None = None,
359
) -> EncoderCudaGraphCaptureInputs:
360
per_image_output = token_budget // max_batch_size
361
grid_config = [
@@ -426,6 +434,7 @@ def _make_manager_for_gpu(
434
mgr.use_dp = False
435
mgr.budget_graphs = {"default": {}}
436
mgr.graph_pool = None
437
+ mgr._capture_axes = ()
438
mgr.graph_hits = 0
439
mgr.graph_misses = 0
440
mgr.log_stats_interval = 100
@@ -656,6 +665,7 @@ class SimpleMockViTVideoModel(SimpleMockViTModel):
665
device: torch.device,
666
dtype: torch.dtype,
667
path: str = "default",
668
+ axis_keys: tuple[Hashable, ...] | None = None,
669
) -> EncoderCudaGraphCaptureInputs:
670
per_item_output = token_budget // max_batch_size
671
frames_per_item = max_frames_per_batch // max_batch_size
vllm/model_executor/models/cohere_compass.py
+2
-1
@@ -24,7 +24,7 @@
24
"""Inference-only Cohere Compass model compatible with HuggingFace weights."""
25
26
import math
27
-from collections.abc import Callable, Iterable, Iterator, Mapping, Sequence
27
+from collections.abc import Callable, Hashable, Iterable, Iterator, Mapping, Sequence
28
from functools import lru_cache, partial
29
from itertools import islice
30
from typing import Annotated, Any, Literal, TypeAlias
@@ -1598,6 +1598,7 @@ class CohereCompassForConditionalGeneration(
1598
device: torch.device,
1599
dtype: torch.dtype,
1600
path: str = "default",
1601
+ axis_keys: tuple[Hashable, ...] | None = None,
1602
):
1603
from vllm.v1.worker.encoder_cudagraph_defs import (
1604
EncoderCudaGraphCaptureInputs,
vllm/model_executor/models/deepseek_ocr.py
+2
-1
@@ -3,7 +3,7 @@
3
"""Inference-only Deepseek-OCR model compatible with HuggingFace weights."""
4
5
import math
6
-from collections.abc import Iterable, Mapping, Sequence
6
+from collections.abc import Hashable, Iterable, Mapping, Sequence
7
from typing import Annotated, Any, Literal
8
9
import torch
@@ -804,6 +804,7 @@ class DeepseekOCRForCausalLM(
804
device: torch.device,
805
dtype: torch.dtype,
806
path: str = "default",
807
+ axis_keys: tuple[Hashable, ...] | None = None,
808
):
809
assert path in ("global", "local")
810
vllm/model_executor/models/ernie45_vl.py
+2
-1
@@ -24,7 +24,7 @@
24
"""Inference-only Ernie VL model compatible with HuggingFace weights."""
25
26
import math
27
-from collections.abc import Callable, Iterable, Iterator, Mapping, Sequence
27
+from collections.abc import Callable, Hashable, Iterable, Iterator, Mapping, Sequence
28
from functools import partial
29
from typing import Annotated, Any, Literal
30
@@ -1681,6 +1681,7 @@ class Ernie4_5_VLMoeForConditionalGeneration(
1681
device,
1682
dtype,
1683
path: str = "default",
1684
+ axis_keys: tuple[Hashable, ...] | None = None,
1685
):
1686
from vllm.v1.worker.encoder_cudagraph_defs import (
1687
EncoderCudaGraphCaptureInputs,
vllm/model_executor/models/gemma3_mm.py
+2
-1
@@ -1,7 +1,7 @@
1
# SPDX-License-Identifier: Apache-2.0
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
import math
4
-from collections.abc import Iterable, Mapping, Sequence
4
+from collections.abc import Hashable, Iterable, Mapping, Sequence
5
from typing import Annotated, Any, Literal
6
7
import torch
@@ -798,6 +798,7 @@ class Gemma3ForConditionalGeneration(
798
device: torch.device,
799
dtype: torch.dtype,
800
path: str = "default",
801
+ axis_keys: tuple[Hashable, ...] | None = None,
802
):
803
from vllm.v1.worker.encoder_cudagraph_defs import (
804
EncoderCudaGraphCaptureInputs,
vllm/model_executor/models/gemma4_mm.py
+2
-1
@@ -15,7 +15,7 @@ reason about temporal order.
15
"""
16
17
import math
18
-from collections.abc import Iterable, Mapping, Sequence
18
+from collections.abc import Hashable, Iterable, Mapping, Sequence
19
from typing import TYPE_CHECKING, Annotated, Any, ClassVar, Literal
20
21
import numpy as np
@@ -1812,6 +1812,7 @@ class Gemma4ForConditionalGeneration(
1812
device: torch.device | str = "cpu",
1813
dtype: torch.dtype | None = None,
1814
path: str = "default",
1815
+ axis_keys: tuple[Hashable, ...] | None = None,
1816
**kwargs: Any,
1817
) -> "EncoderCudaGraphCaptureInputs":
1818
from vllm.v1.worker.encoder_cudagraph_defs import (
vllm/model_executor/models/glm4_1v.py
+2
-1
@@ -28,7 +28,7 @@
28
compatible with HuggingFace weights."""
29
30
import math
31
-from collections.abc import Callable, Iterable, Iterator, Mapping, Sequence
31
+from collections.abc import Callable, Hashable, Iterable, Iterator, Mapping, Sequence
32
from functools import partial
33
from typing import Annotated, Any, Literal, TypeAlias
34
@@ -2077,6 +2077,7 @@ class Glm4vForConditionalGeneration(
2077
device: torch.device,
2078
dtype: torch.dtype,
2079
path: str = "default",
2080
+ axis_keys: tuple[Hashable, ...] | None = None,
2081
):
2082
from vllm.v1.worker.encoder_cudagraph_defs import (
2083
EncoderCudaGraphCaptureInputs,
vllm/model_executor/models/idefics2_vision_model.py
+51
-38
@@ -38,8 +38,7 @@ from vllm.model_executor.layers.linear import (
38
RowParallelLinear,
39
)
40
from vllm.model_executor.layers.quantization import QuantizationConfig
41
-from vllm.utils.gpu_sync_debug import gpu_sync_allowed
42
-from vllm.utils.torch_utils import PIN_MEMORY
41
+from vllm.utils.torch_utils import async_tensor_h2d
42
43
from .utils import AutoWeightsLoader, WeightsMapper
44
from .vision import is_vit_use_data_parallel, run_dp_sharded_vision_model
@@ -81,41 +80,61 @@ class Idefics2VisionEmbeddings(nn.Module):
80
patch_attention_mask: torch.BoolTensor,
81
tgt_sizes: torch.IntTensor | None = None,
82
) -> torch.Tensor:
84
- batch_size, max_nb_patches_h, max_nb_patches_w = patch_attention_mask.shape
83
+ batch_size = patch_attention_mask.shape[0]
84
+ device = patch_attention_mask.device
85
boundaries = torch.arange(
86
- 1 / self.num_patches_per_side, 1.0, 1 / self.num_patches_per_side
87
- )
88
- position_ids = torch.full(
89
- size=(batch_size, max_nb_patches_h * max_nb_patches_w),
90
- fill_value=0,
91
- pin_memory=PIN_MEMORY,
86
+ 1 / self.num_patches_per_side,
87
+ 1.0,
88
+ 1 / self.num_patches_per_side,
89
+ device=device,
90
)
91
94
- with gpu_sync_allowed():
95
- patch_attention_mask_cpu = patch_attention_mask.cpu()
96
- tgt_sizes_cpu = tgt_sizes.cpu() if tgt_sizes is not None else None
92
+ flat_patch_attention_mask = patch_attention_mask.view(batch_size, -1)
93
+ max_patches = flat_patch_attention_mask.shape[-1]
94
98
- for batch_idx, p_attn_mask in enumerate(patch_attention_mask_cpu):
99
- if tgt_sizes_cpu is not None:
100
- nb_patches_h = tgt_sizes_cpu[batch_idx][0]
101
- nb_patches_w = tgt_sizes_cpu[batch_idx][1]
95
+ if tgt_sizes is not None:
96
+ # tgt_sizes may come from CPU-side mm_kwargs; use a pinned async
97
+ # copy to stay clean under VLLM_GPU_SYNC_CHECK.
98
+ if tgt_sizes.device != device:
99
+ tgt_sizes = async_tensor_h2d(tgt_sizes, device)
100
+ nb_patches_h = tgt_sizes[:, 0]
101
+ nb_patches_w = tgt_sizes[:, 1]
102
+ else:
103
+ if patch_attention_mask.dim() == 3:
104
+ nb_patches_h = patch_attention_mask[:, :, 0].sum(dim=1)
105
+ nb_patches_w = patch_attention_mask[:, 0, :].sum(dim=1)
106
else:
103
- nb_patches_h = p_attn_mask[:, 0].sum()
104
- nb_patches_w = p_attn_mask[0].sum()
105
- fractional_coords_h = torch.arange(0, 1 - 1e-6, 1 / nb_patches_h)
106
- fractional_coords_w = torch.arange(0, 1 - 1e-6, 1 / nb_patches_w)
107
- bucket_coords_h = torch.bucketize(
108
- fractional_coords_h, boundaries, right=True
109
- )
110
- bucket_coords_w = torch.bucketize(
111
- fractional_coords_w, boundaries, right=True
112
- )
113
- pos_ids = (
114
- bucket_coords_h[:, None] * self.num_patches_per_side + bucket_coords_w
115
- ).flatten()
116
- position_ids[batch_idx][p_attn_mask.view(-1)] = pos_ids
107
+ # Fallback for unexpected 2D masks without tgt_sizes
108
+ nb_patches_h = torch.ones(batch_size, device=device)
109
+ nb_patches_w = patch_attention_mask.sum(dim=1)
110
+
111
+ nb_h = nb_patches_h.clamp(min=1).unsqueeze(1)
112
+ nb_w = nb_patches_w.clamp(min=1).unsqueeze(1)
113
+
114
+ i = torch.arange(max_patches, device=device)
115
+
116
+ if patch_attention_mask.dim() == 3 and patch_attention_mask.shape[1] > 1:
117
+ stride_w = patch_attention_mask.shape[2] # padded 2D grid width
118
+ else:
119
+ stride_w = nb_w # (batch, 1) — per-item actual patch width
120
+
121
+ h_idx = i.unsqueeze(0) // stride_w
122
+ w_idx = i.unsqueeze(0) % stride_w
123
+
124
+ fractional_h = h_idx / nb_h
125
+ fractional_w = w_idx / nb_w
126
+
127
+ bucket_h = torch.bucketize(fractional_h, boundaries, right=True)
128
+ bucket_w = torch.bucketize(fractional_w, boundaries, right=True)
129
+
130
+ pos_ids = bucket_h * self.num_patches_per_side + bucket_w
131
+
132
+ position_ids = torch.where(flat_patch_attention_mask, pos_ids.to(torch.long), 0)
133
118
- return position_ids.to(self.position_embedding.weight.device, non_blocking=True)
134
+ target_device = self.position_embedding.weight.device
135
+ if position_ids.device != target_device:
136
+ position_ids = async_tensor_h2d(position_ids, target_device)
137
+ return position_ids
138
139
def forward(
140
self,
@@ -461,13 +480,7 @@ class Idefics2VisionTransformer(nn.Module):
480
# - if apply_encoder_attention_mask is False, skip (not all models
481
# sharing this encoder apply masking in attention, e.g. Aria, Phi4)
482
# - if patch_attention_mask was None, skip attention masking
464
- # - if any padding exists, create an additive 4D mask and pass it
465
- # to attention; else skip mask for performance.
466
- if (
467
- not self.apply_encoder_attention_mask
468
- or flat_patch_mask is None
469
- or not torch.any(~flat_patch_mask)
470
- ):
483
+ if not self.apply_encoder_attention_mask or flat_patch_mask is None:
484
attention_mask = None
485
else:
486
# Additive mask: masked positions receive a large negative value.
vllm/model_executor/models/idefics3.py
+2
-1
@@ -16,7 +16,7 @@
16
# limitations under the License.
17
"""Inference-only Idefics3 model compatible with HuggingFace weights."""
18
19
-from collections.abc import Iterable, Mapping, Sequence
19
+from collections.abc import Hashable, Iterable, Mapping, Sequence
20
from typing import Annotated, Any, Literal, TypeAlias
21
22
import torch
@@ -722,6 +722,7 @@ class Idefics3ForConditionalGeneration(
722
device: torch.device,
723
dtype: torch.dtype,
724
path: str = "default",
725
+ axis_keys: tuple[Hashable, ...] | None = None,
726
):
727
from vllm.v1.worker.encoder_cudagraph_defs import (
728
EncoderCudaGraphCaptureInputs,
vllm/model_executor/models/interfaces.py
+15
-1
@@ -7,6 +7,7 @@ from bisect import bisect_right
7
from collections.abc import (
8
AsyncGenerator,
9
Callable,
10
+ Hashable,
11
Mapping,
12
MutableSequence,
13
Sequence,
@@ -1875,6 +1876,11 @@ class SupportsEncoderCudaGraph(Protocol):
1876
- Qwen-family: slice concatenated pixel_values by cumulative
1877
patch offsets, subset grid_thw by indices.
1878
- Batched models (CLIP): index pixel_values along dim 0.
1879
+
1880
+ Models that configure ``EncoderCudaGraphConfig.capture_axes`` must
1881
+ additionally store the resolved per-axis keys (one key per axis, in
1882
+ order) under ``ENCODER_CUDAGRAPH_AXIS_KEYS_KWARG`` in the returned
1883
+ dict; the manager pops it before the kwargs are used elsewhere.
1884
"""
1885
...
1886
@@ -1909,8 +1915,16 @@ class SupportsEncoderCudaGraph(Protocol):
1915
device: torch.device,
1916
dtype: torch.dtype,
1917
path: str = "default",
1918
+ axis_keys: tuple[Hashable, ...] | None = None,
1919
) -> "EncoderCudaGraphCaptureInputs":
1913
- """Create dummy inputs and buffers for CUDA graph capture."""
1920
+ """Create dummy inputs and buffers for CUDA graph capture.
1921
+
1922
+ Args:
1923
+ axis_keys: The resolved capture-axis keys (one per axis of
1924
+ ``EncoderCudaGraphConfig.capture_axes``) this capture is for.
1925
+ None or empty when no capture axes are configured; models
1926
+ without capture axes ignore it.
1927
+ """
1928
...
1929
1930
def prepare_encoder_cudagraph_replay_buffers(
vllm/model_executor/models/internvl.py
+2
-1
@@ -8,7 +8,7 @@
8
# Licensed under The MIT License [see LICENSE for details]
9
# --------------------------------------------------------
10
from abc import abstractmethod
11
-from collections.abc import Iterable, Mapping, Sequence
11
+from collections.abc import Hashable, Iterable, Mapping, Sequence
12
from functools import cached_property
13
from typing import Annotated, Any, Literal, TypeAlias, TypedDict, TypeVar
14
@@ -1028,6 +1028,7 @@ class InternVLChatModel(
1028
device: torch.device,
1029
dtype: torch.dtype,
1030
path: str = "default",
1031
+ axis_keys: tuple[Hashable, ...] | None = None,
1032
):
1033
from vllm.v1.worker.encoder_cudagraph_defs import (
1034
EncoderCudaGraphCaptureInputs,
vllm/model_executor/models/kimi_k25.py
+2
-1
@@ -6,7 +6,7 @@ Kimi-K2.5 Model Implementation for vLLM.
6
Kimi-K2.5 extends Kimi-K2 with vision support.
7
"""
8
9
-from collections.abc import Iterable, Mapping, Sequence
9
+from collections.abc import Hashable, Iterable, Mapping, Sequence
10
from dataclasses import dataclass
11
from typing import TYPE_CHECKING, Annotated, Any, ClassVar, Literal
12
@@ -594,6 +594,7 @@ class KimiK25ForConditionalGeneration(
594
device: torch.device,
595
dtype: torch.dtype,
596
path: str = "default",
597
+ axis_keys: tuple[Hashable, ...] | None = None,
598
) -> "EncoderCudaGraphCaptureInputs":
599
from vllm.v1.worker.encoder_cudagraph_defs import EncoderCudaGraphCaptureInputs
600
vllm/model_executor/models/kimi_vl.py
+2
-1
@@ -43,7 +43,7 @@
43
# SOFTWARE.
44
45
import math
46
-from collections.abc import Iterable, Mapping, Sequence
46
+from collections.abc import Hashable, Iterable, Mapping, Sequence
47
from dataclasses import dataclass
48
from typing import Annotated, Any, Literal
49
@@ -450,6 +450,7 @@ class KimiVLForConditionalGeneration(
450
device: torch.device,
451
dtype: torch.dtype,
452
path: str = "default",
453
+ axis_keys: tuple[Hashable, ...] | None = None,
454
):
455
from vllm.v1.worker.encoder_cudagraph_defs import (
456
EncoderCudaGraphCaptureInputs,
vllm/model_executor/models/lfm2_vl.py
+2
-1
@@ -4,7 +4,7 @@
4
import itertools
5
import math
6
import typing
7
-from collections.abc import Iterable, Mapping, Sequence
7
+from collections.abc import Hashable, Iterable, Mapping, Sequence
8
from typing import Annotated, Any, Literal
9
10
import torch
@@ -1146,6 +1146,7 @@ class Lfm2VLForConditionalGeneration(
1146
device: torch.device,
1147
dtype: torch.dtype,
1148
path: str = "default",
1149
+ axis_keys: tuple[Hashable, ...] | None = None,
1150
):
1151
from vllm.v1.worker.encoder_cudagraph_defs import (
1152
EncoderCudaGraphCaptureInputs,
vllm/model_executor/models/minicpmv.py
+547
-23
@@ -26,10 +26,10 @@
26
27
import math
28
from collections import defaultdict
29
-from collections.abc import Callable, Iterable, Mapping, Sequence, Set
29
+from collections.abc import Callable, Hashable, Iterable, Mapping, Sequence, Set
30
from functools import cached_property, partial
31
from itertools import chain
32
-from typing import Annotated, Any, Literal, TypeAlias
32
+from typing import Annotated, Any, ClassVar, Literal, TypeAlias, cast
33
34
import numpy as np
35
import torch
@@ -100,11 +100,19 @@ from vllm.transformers_utils.utils import convert_model_repo_to_path
100
from vllm.utils.collection_utils import flatten_2d_lists, is_list_of
101
from vllm.utils.gpu_sync_debug import gpu_sync_allowed
102
from vllm.utils.tensor_schema import TensorSchema, TensorShape
103
-from vllm.utils.torch_utils import set_default_torch_dtype
103
+from vllm.utils.torch_utils import async_tensor_h2d, set_default_torch_dtype
104
+from vllm.v1.worker.encoder_cudagraph_defs import (
105
+ ENCODER_CUDAGRAPH_AXIS_KEYS_KWARG,
106
+ EncoderCudaGraphCaptureInputs,
107
+ EncoderCudaGraphConfig,
108
+ EncoderCudaGraphReplayBuffers,
109
+ EncoderItemSpec,
110
+)
111
112
from .idefics2_vision_model import Idefics2VisionTransformer
113
from .interfaces import (
114
MultiModalEmbeddings,
115
+ SupportsEncoderCudaGraph,
116
SupportsLoRA,
117
SupportsMultiModal,
118
SupportsPP,
@@ -218,29 +226,36 @@ class Resampler2_5(BaseResampler):
226
device = x.device
227
dtype = x.dtype
228
229
+ # Only adjust the pos cache in eager mode; during CUDA graph capture
230
+ # the cache was already expanded by the preceding warmup run
231
+ if not torch.cuda.is_current_stream_capturing():
232
+ self._adjust_pos_cache(tgt_sizes, device=device)
233
+
234
+ # tgt_sizes is pinned-copied only when it comes from the CPU-side
235
+ # mm_kwargs; during capture/replay it is already a GPU buffer.
236
+ if tgt_sizes.device != x.device:
237
+ tgt_sizes = async_tensor_h2d(tgt_sizes, x.device)
238
patch_len = tgt_sizes[:, 0] * tgt_sizes[:, 1]
239
+ max_patch_len = x.shape[1]
240
223
- self._adjust_pos_cache(tgt_sizes, device=device)
241
+ seq_idx = torch.arange(max_patch_len, device=device).unsqueeze(0)
242
225
- max_patch_len = patch_len.max().item()
226
- assert isinstance(max_patch_len, int)
243
+ tgt_w = tgt_sizes[:, 1].unsqueeze(1)
244
+ tgt_w = torch.clamp(tgt_w, min=1)
245
228
- key_padding_mask = torch.zeros(
229
- (bs, max_patch_len), dtype=torch.bool, device=device
230
- )
246
+ h_idx = seq_idx // tgt_w
247
+ w_idx = seq_idx % tgt_w
248
232
- pos_embed = []
233
- for i in range(bs):
234
- tgt_h, tgt_w = tgt_sizes[i].tolist()
235
- pos_embed.append(
236
- self.pos_embed[:tgt_h, :tgt_w, :]
237
- .reshape((tgt_h * tgt_w, -1))
238
- .to(device=device, dtype=dtype)
239
- ) # patches * D
240
- key_padding_mask[i, patch_len[i] :] = True
241
- pos_embed = torch.nn.utils.rnn.pad_sequence(
242
- pos_embed, batch_first=True, padding_value=0.0
249
+ h_idx = torch.clamp(h_idx, max=self.pos_embed.shape[0] - 1)
250
+ w_idx = torch.clamp(w_idx, max=self.pos_embed.shape[1] - 1)
251
+
252
+ key_padding_mask = seq_idx >= patch_len.unsqueeze(1)
253
+
254
+ pos_embed = self.pos_embed[h_idx, w_idx].to(dtype)
255
+ pos_embed = torch.where(
256
+ key_padding_mask.unsqueeze(-1), torch.zeros_like(pos_embed), pos_embed
257
).permute(1, 0, 2) # BLD => L * B * D
258
+
259
x, _ = self.kv_proj(x) # B * L * D
260
x = self.ln_kv(x).permute(1, 0, 2) # L * B * D
261
@@ -808,6 +823,16 @@ class MiniCPMVProcessingInfo(BaseProcessingInfo):
823
824
return max(max_frames_per_video, 1)
825
826
+ def get_mm_max_tokens_per_item(
827
+ self,
828
+ seq_len: int,
829
+ mm_counts: Mapping[str, int],
830
+ ) -> Mapping[str, int]:
831
+ max_tokens = {"image": self.get_max_image_tokens()}
832
+ if "video" in self.get_supported_mm_limits():
833
+ max_tokens["video"] = self.get_max_video_tokens(seq_len, mm_counts)
834
+ return max_tokens
835
+
836
837
_I = TypeVar("_I", bound=MiniCPMVProcessingInfo, default=MiniCPMVProcessingInfo)
838
@@ -1223,6 +1248,7 @@ class MiniCPMVBaseModel(nn.Module, SupportsMultiModal, SupportsPP):
1248
# and config class
1249
self.config = config
1250
self.multimodal_config = multimodal_config
1251
+ self.vllm_config = vllm_config
1252
1253
self.version = get_version_by_config(self.config)
1254
@@ -1428,6 +1454,504 @@ class MiniCPMVBaseModel(nn.Module, SupportsMultiModal, SupportsPP):
1454
raise NotImplementedError
1455
1456
1457
+# --- Encoder CUDA graph (MiniCPM-V 2.5 / 2.6 / 4.0; mixed into subclasses) ---
1458
+# Buffer keys
1459
+_MINICPMV_CUDAGRAPH_BUF_KEY_TGT_SIZES = "minicpmv_tgt_sizes"
1460
+_MINICPMV_CUDAGRAPH_BUF_KEY_PATCH_MASK = "minicpmv_patch_attn_mask"
1461
+_MINICPMV_CUDAGRAPH_BUF_KEY_PIXEL = "minicpmv_pixel_values"
1462
+
1463
+# mm_kwargs keys for the flat pixel-value tensor
1464
+_MINICPMV_CUDAGRAPH_FLAT_KEY_IMAGE = "minicpmv_encoder_input_flat"
1465
+_MINICPMV_CUDAGRAPH_FLAT_KEY_VIDEO = "minicpmv_video_encoder_input_flat"
1466
+# Resolved patch-grid capture-axis key ``(nb_h, nb_w)`` of the sub-batch;
1467
+# pixel dims and max patches are derived from it and the patch size.
1468
+_MINICPMV_CUDAGRAPH_PATCH_GRID_KEY = "minicpmv_encoder_patch_grid"
1469
+
1470
+_MINICPMV_BASE_PATCH_BUCKETS: tuple[tuple[int, int], ...] = (
1471
+ (32, 32),
1472
+ (32, 64),
1473
+ (64, 32),
1474
+)
1475
+
1476
+_ENCODER_CUDAGRAPH_MM_KWARGS_SKIP_KEYS = frozenset(
1477
+ {
1478
+ _MINICPMV_CUDAGRAPH_FLAT_KEY_IMAGE,
1479
+ _MINICPMV_CUDAGRAPH_FLAT_KEY_VIDEO,
1480
+ _MINICPMV_CUDAGRAPH_PATCH_GRID_KEY,
1481
+ }
1482
+)
1483
+
1484
+
1485
+def _mcpmv_tgt_sizes_tensor(
1486
+ mm_kwargs: dict[str, Any], *, video: bool
1487
+) -> torch.Tensor | list[torch.Tensor]:
1488
+ key = "video_tgt_sizes" if video else "tgt_sizes"
1489
+ return mm_kwargs[key]
1490
+
1491
+
1492
+def _mcpmv_normalize_tgt_sizes(
1493
+ tgt_sizes: torch.Tensor | list[torch.Tensor],
1494
+ slice_counts: list[int],
1495
+) -> torch.Tensor:
1496
+ """Normalize tgt_sizes to shape ``(total_slices, 2)``."""
1497
+
1498
+ if isinstance(tgt_sizes, list):
1499
+ if not tgt_sizes:
1500
+ tgt_sizes = torch.zeros((0, 2), dtype=torch.long)
1501
+ else:
1502
+ tgt_sizes = torch.cat(tgt_sizes, dim=0)
1503
+
1504
+ total_slices = sum(slice_counts)
1505
+ if tgt_sizes.dim() == 2 and tgt_sizes.shape[0] == total_slices:
1506
+ return tgt_sizes
1507
+ if tgt_sizes.dim() == 3:
1508
+ return torch.cat(
1509
+ [tgt_sizes[i, : slice_counts[i], :] for i in range(len(slice_counts))],
1510
+ dim=0,
1511
+ )
1512
+ return torch.repeat_interleave(
1513
+ tgt_sizes,
1514
+ torch.tensor(slice_counts, device=tgt_sizes.device),
1515
+ dim=0,
1516
+ )
1517
+
1518
+
1519
+def _mcpmv_pack_flat_pixels(
1520
+ slices: list[torch.Tensor],
1521
+ *,
1522
+ pixel_height: int,
1523
+ pixel_width: int,
1524
+ max_num_slices: int,
1525
+ device: torch.device,
1526
+ dtype: torch.dtype,
1527
+ patch_size: int,
1528
+ tgt_sizes: torch.Tensor,
1529
+) -> torch.Tensor:
1530
+ """Pack slice tensors into a fixed ``(max_num_slices, 3*H*W)`` buffer."""
1531
+ import torch.nn.functional as F
1532
+
1533
+ flat_dim = 3 * pixel_height * pixel_width
1534
+ grid_h = pixel_height // patch_size
1535
+ grid_w = pixel_width // patch_size
1536
+ max_grid_patches = grid_h * grid_w
1537
+
1538
+ packed = torch.zeros((max_num_slices, flat_dim), device=device, dtype=dtype)
1539
+ n = min(len(slices), max_num_slices)
1540
+ tgt_list = tgt_sizes.tolist()
1541
+ for i, slc in enumerate(slices[:n]):
1542
+ slc = slc.to(dtype=dtype, device=device)
1543
+ C, h, w = slc.shape
1544
+ if h == patch_size:
1545
+ nb_h, nb_w = int(tgt_list[i][0]), int(tgt_list[i][1])
1546
+ num_patches = nb_h * nb_w
1547
+
1548
+ patches = (
1549
+ slc.view(C, patch_size, nb_h, nb_w, patch_size)
1550
+ .permute(0, 2, 3, 1, 4)
1551
+ .reshape(C, num_patches, patch_size, patch_size)
1552
+ )
1553
+
1554
+ col = patches.permute(0, 2, 3, 1).reshape(
1555
+ 1, C * patch_size * patch_size, num_patches
1556
+ )
1557
+ col_padded = col.new_zeros(1, C * patch_size * patch_size, max_grid_patches)
1558
+ col_padded[..., :num_patches] = col
1559
+
1560
+ folded = F.fold(
1561
+ col_padded,
1562
+ output_size=(pixel_height, pixel_width),
1563
+ kernel_size=patch_size,
1564
+ stride=patch_size,
1565
+ )
1566
+ packed[i] = folded.reshape(-1)
1567
+ else:
1568
+ packed[i].view(3, pixel_height, pixel_width)[:, :h, :w] = slc
1569
+ return packed
1570
+
1571
+
1572
+class _MiniCPMVEncoderCudaGraphMixin(MiniCPMVBaseModel, SupportsEncoderCudaGraph):
1573
+ """SupportsEncoderCudaGraph for MiniCPM-V Idefics2 + resampler (not 2.0)."""
1574
+
1575
+ supports_encoder_cudagraph: ClassVar[Literal[True]] = True
1576
+
1577
+ def _mcpmv_slice_pixel_size(self) -> tuple[int, int]:
1578
+ image_size = int(self.vpm.embeddings.image_size)
1579
+ return image_size, image_size
1580
+
1581
+ def _mcpmv_patch_grid_pixel_hw(
1582
+ self, patch_grid_key: tuple[int, int]
1583
+ ) -> tuple[int, int]:
1584
+ patch_size = int(self.vpm.embeddings.patch_size)
1585
+ th, tw = patch_grid_key
1586
+ return th * patch_size, tw * patch_size
1587
+
1588
+ def _mcpmv_patch_grid_num_patches(self, patch_grid_key: tuple[int, int]) -> int:
1589
+ th, tw = patch_grid_key
1590
+ return th * tw
1591
+
1592
+ def _mcpmv_patch_grid_keys(self) -> tuple[tuple[int, int], ...]:
1593
+ """Ordered patch-grid keys ``(nb_h, nb_w)`` for the capture axis."""
1594
+ max_side = int(self.vpm.embeddings.image_size) // int(
1595
+ self.vpm.embeddings.patch_size
1596
+ )
1597
+ keys: list[tuple[int, int]] = []
1598
+ seen: set[tuple[int, int]] = set()
1599
+ for th, tw in _MINICPMV_BASE_PATCH_BUCKETS:
1600
+ if th <= max_side and tw <= max_side:
1601
+ keys.append((th, tw))
1602
+ seen.add((th, tw))
1603
+ full = (max_side, max_side)
1604
+ if full not in seen:
1605
+ keys.append(full)
1606
+ return tuple(keys)
1607
+
1608
+ def _mcpmv_resolve_patch_grid(
1609
+ self,
1610
+ tgt_sizes: torch.Tensor,
1611
+ slice_counts: list[int],
1612
+ indices: list[int],
1613
+ ) -> tuple[int, int]:
1614
+ """Smallest patch-grid key covering all selected items."""
1615
+ keys = self._mcpmv_patch_grid_keys()
1616
+ if not indices:
1617
+ return keys[0]
1618
+ tgt_groups = torch.split(tgt_sizes, slice_counts)
1619
+ selected = torch.cat([tgt_groups[i] for i in indices], dim=0)
1620
+ need_h = int(selected[:, 0].max().item())
1621
+ need_w = int(selected[:, 1].max().item())
1622
+ for th, tw in keys:
1623
+ if th >= need_h and tw >= need_w:
1624
+ return (th, tw)
1625
+ return keys[-1]
1626
+
1627
+ def _mcpmv_max_slices_cap(
1628
+ self,
1629
+ token_budget: int,
1630
+ max_batch_size: int,
1631
+ max_frames_per_batch: int,
1632
+ ) -> int:
1633
+ max_slice_num = int(getattr(self.config, "max_slice_num", 9))
1634
+ query_num = max(1, int(self.config.query_num))
1635
+ max_slices_by_token_budget = max(1, token_budget // query_num)
1636
+ max_slices_by_content = max_batch_size * (max_slice_num + 1)
1637
+ if self.version in {(2, 6), (4, 0)} and max_frames_per_batch > 0:
1638
+ max_slices_by_content = max(
1639
+ max_slices_by_content,
1640
+ max_frames_per_batch * (max_slice_num + 1),
1641
+ )
1642
+ return max(1, min(max_slices_by_token_budget, max_slices_by_content))
1643
+
1644
+ def get_encoder_cudagraph_config(self) -> EncoderCudaGraphConfig:
1645
+ buffer_keys = [
1646
+ _MINICPMV_CUDAGRAPH_BUF_KEY_PIXEL,
1647
+ _MINICPMV_CUDAGRAPH_BUF_KEY_TGT_SIZES,
1648
+ _MINICPMV_CUDAGRAPH_BUF_KEY_PATCH_MASK,
1649
+ ]
1650
+ # Video is only supported from 2.6 onward.
1651
+ modalities = ["image"]
1652
+ if self.version in {(2, 6), (4, 0)}:
1653
+ modalities.append("video")
1654
+
1655
+ max_frames = self.get_max_frames_per_video() if "video" in modalities else 1
1656
+
1657
+ return EncoderCudaGraphConfig(
1658
+ modalities=modalities,
1659
+ buffer_keys=buffer_keys,
1660
+ out_hidden_size=int(self.embed_dim),
1661
+ max_frames_per_video=max_frames,
1662
+ capture_axes=(self._mcpmv_patch_grid_keys(),),
1663
+ )
1664
+
1665
+ def get_input_modality(self, mm_kwargs: dict[str, Any]) -> str:
1666
+ if "video_pixel_values" in mm_kwargs:
1667
+ return "video"
1668
+ return "image"
1669
+
1670
+ def get_max_frames_per_video(self) -> int:
1671
+ info = MULTIMODAL_REGISTRY.get_processing_info(self.vllm_config.model_config)
1672
+ assert isinstance(info, MiniCPMVProcessingInfo)
1673
+ return int(
1674
+ info.get_num_frames_with_most_features(
1675
+ seq_len=self.vllm_config.model_config.max_model_len,
1676
+ mm_counts={
1677
+ "video": self.multimodal_config.get_limit_per_prompt("video")
1678
+ },
1679
+ )
1680
+ )
1681
+
1682
+ def get_encoder_cudagraph_budget_range(
1683
+ self, vllm_config: VllmConfig
1684
+ ) -> tuple[int, int]:
1685
+ # Each slice produces exactly query_num resampler output tokens.
1686
+ # A thumbnail-only image has 1 slice, so query_num is the smallest
1687
+ # possible encoder output and the natural minimum budget.
1688
+ min_budget = int(self.config.query_num)
1689
+ max_budget = min(
1690
+ vllm_config.scheduler_config.max_num_batched_tokens,
1691
+ vllm_config.model_config.max_model_len,
1692
+ )
1693
+ return (min_budget, max_budget)
1694
+
1695
+ def get_encoder_cudagraph_item_specs(
1696
+ self, mm_kwargs: dict[str, Any]
1697
+ ) -> list[EncoderItemSpec]:
1698
+ video = self.get_input_modality(mm_kwargs) == "video"
1699
+ pixel_values_key = "video_pixel_values" if video else "pixel_values"
1700
+ pixel_values: list[list[torch.Tensor]] = mm_kwargs[pixel_values_key]
1701
+ slice_counts = [len(img) for img in pixel_values]
1702
+ tgt_sizes = _mcpmv_tgt_sizes_tensor(mm_kwargs, video=video)
1703
+ tgt_sizes = _mcpmv_normalize_tgt_sizes(tgt_sizes, slice_counts)
1704
+ patch_sums = tgt_sizes.prod(-1)
1705
+ input_sizes = [
1706
+ int(group.sum().item()) for group in torch.split(patch_sums, slice_counts)
1707
+ ]
1708
+ query_num = int(self.config.query_num)
1709
+ return [
1710
+ EncoderItemSpec(
1711
+ input_size=input_sizes[i],
1712
+ output_tokens=slice_counts[i] * query_num,
1713
+ )
1714
+ for i in range(len(pixel_values))
1715
+ ]
1716
+
1717
+ def select_encoder_cudagraph_items(
1718
+ self,
1719
+ mm_kwargs: dict[str, Any],
1720
+ indices: list[int],
1721
+ ) -> dict[str, Any]:
1722
+ subset, patch_grid = self._mcpmv_select_items(mm_kwargs, indices)
1723
+ subset[ENCODER_CUDAGRAPH_AXIS_KEYS_KWARG] = (patch_grid,)
1724
+ return subset
1725
+
1726
+ def _mcpmv_select_items(
1727
+ self, mm_kwargs: dict[str, Any], indices: list[int]
1728
+ ) -> tuple[dict[str, Any], tuple[int, int]]:
1729
+ """Slice mm_kwargs for `indices`; also returns the patch-grid key."""
1730
+ video = self.get_input_modality(mm_kwargs) == "video"
1731
+ pixel_values_key = "video_pixel_values" if video else "pixel_values"
1732
+ tgt_key = "video_tgt_sizes" if video else "tgt_sizes"
1733
+ flat_key = (
1734
+ _MINICPMV_CUDAGRAPH_FLAT_KEY_VIDEO
1735
+ if video
1736
+ else _MINICPMV_CUDAGRAPH_FLAT_KEY_IMAGE
1737
+ )
1738
+ device = next(self.vpm.parameters()).device
1739
+ pixel_values: list[list[torch.Tensor]] = mm_kwargs[pixel_values_key]
1740
+ tgt_sizes = _mcpmv_tgt_sizes_tensor(mm_kwargs, video=video)
1741
+
1742
+ subset = {
1743
+ k: v
1744
+ for k, v in mm_kwargs.items()
1745
+ if k not in _ENCODER_CUDAGRAPH_MM_KWARGS_SKIP_KEYS
1746
+ }
1747
+
1748
+ if not indices:
1749
+ pixel_h, pixel_w = self._mcpmv_slice_pixel_size()
1750
+ vpm_dtype = next(self.vpm.parameters()).dtype
1751
+ subset.update(
1752
+ {
1753
+ pixel_values_key: [],
1754
+ tgt_key: torch.zeros((0, 2), dtype=torch.long, device=device),
1755
+ flat_key: torch.zeros(
1756
+ (0, 3 * pixel_h * pixel_w), device=device, dtype=vpm_dtype
1757
+ ),
1758
+ }
1759
+ )
1760
+ return subset, self._mcpmv_patch_grid_keys()[0]
1761
+
1762
+ slice_counts = [len(item_slices) for item_slices in pixel_values]
1763
+ tgt_sizes = _mcpmv_normalize_tgt_sizes(tgt_sizes, slice_counts)
1764
+ tgt_groups = torch.split(tgt_sizes, slice_counts)
1765
+
1766
+ patch_grid = self._mcpmv_resolve_patch_grid(tgt_sizes, slice_counts, indices)
1767
+ pixel_h, pixel_w = self._mcpmv_patch_grid_pixel_hw(patch_grid)
1768
+
1769
+ selected_pixel_values = [pixel_values[i] for i in indices]
1770
+ selected_tgt_sizes_list = [tgt_groups[i] for i in indices]
1771
+ selected_tgt_sizes = torch.cat(selected_tgt_sizes_list, dim=0)
1772
+
1773
+ selected_slices = flatten_2d_lists(selected_pixel_values)
1774
+ packed_flat_pixels = _mcpmv_pack_flat_pixels(
1775
+ selected_slices,
1776
+ pixel_height=pixel_h,
1777
+ pixel_width=pixel_w,
1778
+ max_num_slices=len(selected_slices),
1779
+ device=selected_slices[0].device,
1780
+ dtype=selected_slices[0].dtype,
1781
+ patch_size=int(self.vpm.embeddings.patch_size),
1782
+ tgt_sizes=selected_tgt_sizes,
1783
+ )
1784
+
1785
+ subset.update(
1786
+ {
1787
+ pixel_values_key: selected_pixel_values,
1788
+ tgt_key: selected_tgt_sizes_list,
1789
+ flat_key: packed_flat_pixels,
1790
+ _MINICPMV_CUDAGRAPH_PATCH_GRID_KEY: patch_grid,
1791
+ }
1792
+ )
1793
+ return subset, patch_grid
1794
+
1795
+ def prepare_encoder_cudagraph_capture_inputs(
1796
+ self,
1797
+ token_budget: int,
1798
+ max_batch_size: int,
1799
+ max_frames_per_batch: int,
1800
+ device: torch.device,
1801
+ dtype: torch.dtype,
1802
+ path: str = "default",
1803
+ axis_keys: tuple[Hashable, ...] | None = None,
1804
+ ):
1805
+ patch_grid = (
1806
+ cast("tuple[int, int]", axis_keys[0])
1807
+ if axis_keys
1808
+ # Without capture-axis context, use the largest (full-resolution)
1809
+ # patch grid.
1810
+ else self._mcpmv_patch_grid_keys()[-1]
1811
+ )
1812
+ return self._mcpmv_capture_inputs(
1813
+ token_budget,
1814
+ max_batch_size,
1815
+ max_frames_per_batch,
1816
+ device,
1817
+ dtype,
1818
+ patch_grid=patch_grid,
1819
+ )
1820
+
1821
+ def _mcpmv_capture_inputs(
1822
+ self,
1823
+ token_budget: int,
1824
+ max_batch_size: int,
1825
+ max_frames_per_batch: int,
1826
+ device: torch.device,
1827
+ dtype: torch.dtype,
1828
+ patch_grid: tuple[int, int],
1829
+ ):
1830
+ th, tw = patch_grid
1831
+ pixel_h, pixel_w = self._mcpmv_patch_grid_pixel_hw(patch_grid)
1832
+ max_patches = self._mcpmv_patch_grid_num_patches(patch_grid)
1833
+ max_num_slices = self._mcpmv_max_slices_cap(
1834
+ token_budget,
1835
+ max_batch_size,
1836
+ max_frames_per_batch,
1837
+ )
1838
+ pixel_buffer = torch.zeros(
1839
+ (max_num_slices, 3, pixel_h, pixel_w), device=device, dtype=dtype
1840
+ )
1841
+ dummy_tgt_sizes = torch.zeros(
1842
+ (max_num_slices, 2), dtype=torch.long, device=device
1843
+ )
1844
+ dummy_tgt_sizes[:, 0] = th
1845
+ dummy_tgt_sizes[:, 1] = tw
1846
+ dummy_patch_mask = torch.ones(
1847
+ (max_num_slices, max_patches), dtype=torch.bool, device=device
1848
+ )
1849
+ values: dict[str, torch.Tensor] = {
1850
+ _MINICPMV_CUDAGRAPH_BUF_KEY_PIXEL: pixel_buffer,
1851
+ _MINICPMV_CUDAGRAPH_BUF_KEY_TGT_SIZES: dummy_tgt_sizes,
1852
+ _MINICPMV_CUDAGRAPH_BUF_KEY_PATCH_MASK: dummy_patch_mask,
1853
+ }
1854
+ return EncoderCudaGraphCaptureInputs(values=values)
1855
+
1856
+ def prepare_encoder_cudagraph_replay_buffers(
1857
+ self,
1858
+ mm_kwargs: dict[str, Any],
1859
+ max_batch_size: int,
1860
+ max_frames_per_batch: int,
1861
+ path: str = "default",
1862
+ ):
1863
+ _ = max_batch_size
1864
+ _ = max_frames_per_batch
1865
+ video = self.get_input_modality(mm_kwargs) == "video"
1866
+ flat_key = (
1867
+ _MINICPMV_CUDAGRAPH_FLAT_KEY_VIDEO
1868
+ if video
1869
+ else _MINICPMV_CUDAGRAPH_FLAT_KEY_IMAGE
1870
+ )
1871
+ flat_pixels = mm_kwargs[flat_key] # (num_actual_slices, 3*pixel_h*pixel_w)
1872
+ patch_grid = mm_kwargs[_MINICPMV_CUDAGRAPH_PATCH_GRID_KEY]
1873
+ pixel_h, pixel_w = self._mcpmv_patch_grid_pixel_hw(patch_grid)
1874
+ max_patches = self._mcpmv_patch_grid_num_patches(patch_grid)
1875
+ pixel_buffer = flat_pixels.reshape(-1, 3, pixel_h, pixel_w)
1876
+
1877
+ device = next(self.vpm.parameters()).device
1878
+ tgt_sizes_raw = _mcpmv_tgt_sizes_tensor(mm_kwargs, video=video)
1879
+ if isinstance(tgt_sizes_raw, list):
1880
+ tgt_sizes_raw = torch.cat(tgt_sizes_raw, dim=0)
1881
+ # tgt_sizes arrives from CPU-side mm_kwargs; use a pinned async copy
1882
+ # to stay clean under VLLM_GPU_SYNC_CHECK.
1883
+ tgt_sizes = async_tensor_h2d(tgt_sizes_raw, device, dtype=torch.long)
1884
+
1885
+ patches_per_slice = tgt_sizes.prod(-1).clamp(max=max_patches)
1886
+ col_idx = torch.arange(max_patches, device=device)
1887
+ patch_attention_mask = col_idx.unsqueeze(0) < patches_per_slice.unsqueeze(1)
1888
+
1889
+ values: dict[str, torch.Tensor] = {
1890
+ _MINICPMV_CUDAGRAPH_BUF_KEY_PIXEL: pixel_buffer,
1891
+ _MINICPMV_CUDAGRAPH_BUF_KEY_TGT_SIZES: tgt_sizes,
1892
+ _MINICPMV_CUDAGRAPH_BUF_KEY_PATCH_MASK: patch_attention_mask,
1893
+ }
1894
+ return EncoderCudaGraphReplayBuffers(values=values)
1895
+
1896
+ def encoder_cudagraph_forward(
1897
+ self,
1898
+ values: dict[str, torch.Tensor],
1899
+ path: str = "default",
1900
+ ) -> torch.Tensor:
1901
+ all_pixel_values = values[_MINICPMV_CUDAGRAPH_BUF_KEY_PIXEL]
1902
+ tgt_sizes = values[_MINICPMV_CUDAGRAPH_BUF_KEY_TGT_SIZES]
1903
+ patch_mask = values[_MINICPMV_CUDAGRAPH_BUF_KEY_PATCH_MASK]
1904
+ patch_attention_mask = patch_mask.unsqueeze(1)
1905
+
1906
+ max_num_slices = all_pixel_values.shape[0]
1907
+ # v2.5 infers patch layout from the attention mask; pass tgt_sizes=None.
1908
+ vpm_tgt_sizes = None if self.version == (2, 5) else tgt_sizes
1909
+ vision_embedding = self.vpm(
1910
+ all_pixel_values,
1911
+ patch_attention_mask=patch_attention_mask,
1912
+ tgt_sizes=vpm_tgt_sizes,
1913
+ )
1914
+
1915
+ resampler_out = self.resampler(vision_embedding, tgt_sizes)
1916
+
1917
+ query_num = int(self.config.query_num)
1918
+ return resampler_out.reshape(max_num_slices * query_num, int(self.embed_dim))
1919
+
1920
+ def encoder_eager_forward(
1921
+ self,
1922
+ mm_kwargs: dict[str, Any],
1923
+ path: str = "default",
1924
+ ) -> torch.Tensor:
1925
+ """Eager encoder path; returns ``(total_tokens, embed_dim)`` like
1926
+ ``encoder_cudagraph_forward``.
1927
+ """
1928
+ mm_kwargs_no_flat = {
1929
+ k: v
1930
+ for k, v in mm_kwargs.items()
1931
+ if k not in _ENCODER_CUDAGRAPH_MM_KWARGS_SKIP_KEYS
1932
+ }
1933
+ modalities = self._parse_and_validate_multimodal_inputs(**mm_kwargs_no_flat)
1934
+ segments: list[torch.Tensor] = []
1935
+ embed_dim = self.embed_dim
1936
+ for modality in modalities:
1937
+ if modality == "images":
1938
+ image_input = modalities["images"]
1939
+ assert isinstance(image_input, MiniCPMVImagePixelInputs)
1940
+ image_embeddings = self.get_vision_hidden_states(image_input)
1941
+ segments.append(image_embeddings.reshape(-1, embed_dim))
1942
+ elif modality == "videos":
1943
+ video_input = modalities["videos"]
1944
+ assert isinstance(video_input, MiniCPMVImagePixelInputs)
1945
+ video_embeddings = self.get_vision_hidden_states(video_input)
1946
+ segments.append(video_embeddings.reshape(-1, embed_dim))
1947
+ if not segments:
1948
+ raise RuntimeError(
1949
+ "MiniCPM-V encoder cudagraph eager path expects pixel_values "
1950
+ "or video_pixel_values"
1951
+ )
1952
+ return torch.cat(segments, dim=0)
1953
+
1954
+
1955
class MiniCPMV2_0(MiniCPMVBaseModel):
1956
supports_encoder_tp_data = False
1957
@@ -1519,7 +2043,7 @@ class MiniCPMV2_0(MiniCPMVBaseModel):
2043
return torch.vstack(res)
2044
2045
1522
-class MiniCPMV2_5(MiniCPMVBaseModel, SupportsLoRA):
2046
+class MiniCPMV2_5(_MiniCPMVEncoderCudaGraphMixin, SupportsLoRA):
2047
packed_modules_mapping = {
2048
"qkv_proj": [
2049
"q_proj",
@@ -1611,7 +2135,7 @@ class MiniCPMV2_5(MiniCPMVBaseModel, SupportsLoRA):
2135
return self.resampler(vision_embedding, tgt_sizes)
2136
2137
1614
-class MiniCPMV2_6(MiniCPMVBaseModel, SupportsLoRA):
2138
+class MiniCPMV2_6(_MiniCPMVEncoderCudaGraphMixin, SupportsLoRA):
2139
hf_to_vllm_mapper = WeightsMapper(
2140
# The vision-only models have no audio tower or TTS head to load weights into.
2141
orig_to_new_prefix={"apm.": None, "audio": None, "tts": None}
@@ -1715,7 +2239,7 @@ class MiniCPMV2_6(MiniCPMVBaseModel, SupportsLoRA):
2239
return loaded
2240
2241
1718
-class MiniCPMV4_0(MiniCPMVBaseModel, SupportsLoRA):
2242
+class MiniCPMV4_0(_MiniCPMVEncoderCudaGraphMixin, SupportsLoRA):
2243
hf_to_vllm_mapper = MiniCPMV2_6.hf_to_vllm_mapper
2244
packed_modules_mapping = {
2245
"qkv_proj": [
vllm/model_executor/models/mllama4.py
+2
-1
@@ -17,7 +17,7 @@
17
# See the License for the specific language governing permissions and
18
# limitations under the License.
19
import math
20
-from collections.abc import Iterable, Mapping
20
+from collections.abc import Hashable, Iterable, Mapping
21
from itertools import tee
22
from typing import Annotated, Any, Literal
23
@@ -924,6 +924,7 @@ class Llama4ForConditionalGeneration(
924
device: torch.device,
925
dtype: torch.dtype,
926
path: str = "default",
927
+ axis_keys: tuple[Hashable, ...] | None = None,
928
):
929
from vllm.v1.worker.encoder_cudagraph_defs import (
930
EncoderCudaGraphCaptureInputs,
vllm/model_executor/models/qwen2_5_vl.py
+2
-1
@@ -26,7 +26,7 @@
26
# limitations under the License.
27
"""Inference-only Qwen2.5-VL model compatible with HuggingFace weights."""
28
29
-from collections.abc import Callable, Iterable, Iterator, Mapping, Sequence
29
+from collections.abc import Callable, Hashable, Iterable, Iterator, Mapping, Sequence
30
from functools import partial
31
from typing import Annotated, Any, Literal, TypeAlias
32
@@ -1856,6 +1856,7 @@ class Qwen2_5_VLForConditionalGeneration(
1856
device: torch.device,
1857
dtype: torch.dtype,
1858
path: str = "default",
1859
+ axis_keys: tuple[Hashable, ...] | None = None,
1860
):
1861
from vllm.v1.worker.encoder_cudagraph_defs import (
1862
EncoderCudaGraphCaptureInputs,
vllm/model_executor/models/qwen2_vl.py
+2
-1
@@ -26,7 +26,7 @@
26
"""Inference-only Qwen2-VL model compatible with HuggingFace weights."""
27
28
import math
29
-from collections.abc import Callable, Iterable, Iterator, Mapping, Sequence
29
+from collections.abc import Callable, Hashable, Iterable, Iterator, Mapping, Sequence
30
from functools import partial
31
from typing import Annotated, Any, Literal, TypeAlias
32
@@ -1589,6 +1589,7 @@ class Qwen2VLForConditionalGeneration(
1589
device: torch.device,
1590
dtype: torch.dtype,
1591
path: str = "default",
1592
+ axis_keys: tuple[Hashable, ...] | None = None,
1593
):
1594
from vllm.v1.worker.encoder_cudagraph_defs import (
1595
EncoderCudaGraphCaptureInputs,
vllm/model_executor/models/qwen3_vl.py
+2
-1
@@ -24,7 +24,7 @@
24
# limitations under the License.
25
"""Inference-only Qwen3VL model compatible with HuggingFace weights."""
26
27
-from collections.abc import Callable, Iterable, Iterator, Mapping, Sequence
27
+from collections.abc import Callable, Hashable, Iterable, Iterator, Mapping, Sequence
28
from functools import lru_cache, partial
29
from itertools import islice
30
from typing import Any
@@ -2120,6 +2120,7 @@ class Qwen3VLForConditionalGeneration(
2120
device: torch.device,
2121
dtype: torch.dtype,
2122
path: str = "default",
2123
+ axis_keys: tuple[Hashable, ...] | None = None,
2124
):
2125
from vllm.v1.worker.encoder_cudagraph_defs import (
2126
EncoderCudaGraphCaptureInputs,
vllm/model_executor/models/step3_vl.py
+2
-1
@@ -1,7 +1,7 @@
1
# SPDX-License-Identifier: Apache-2.0
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
import math
4
-from collections.abc import Iterable, Mapping, Sequence
4
+from collections.abc import Hashable, Iterable, Mapping, Sequence
5
from math import sqrt
6
from typing import Annotated, Any, Literal, TypeAlias
7
@@ -828,6 +828,7 @@ class Step3VLForConditionalGeneration(
828
device: torch.device,
829
dtype: torch.dtype,
830
path: str = "default",
831
+ axis_keys: tuple[Hashable, ...] | None = None,
832
):
833
from vllm.v1.worker.encoder_cudagraph_defs import (
834
EncoderCudaGraphCaptureInputs,
vllm/models/kimi_k3/nvidia/model.py
+2
-1
@@ -3,7 +3,7 @@
3
"""Kimi-K3 multimodal model implementation for vLLM."""
4
5
import math
6
-from collections.abc import Iterable
6
+from collections.abc import Hashable, Iterable
7
from typing import Any, cast
8
9
import torch
@@ -1948,6 +1948,7 @@ class KimiK3ForConditionalGeneration(
1948
device: torch.device,
1949
dtype: torch.dtype,
1950
path: str = "default",
1951
+ axis_keys: tuple[Hashable, ...] | None = None,
1952
):
1953
from vllm.v1.worker.encoder_cudagraph_defs import (
1954
EncoderCudaGraphCaptureInputs,
vllm/v1/worker/encoder_cudagraph.py
+100
-23
@@ -2,8 +2,11 @@
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
"""CUDA graph manager for vision encoder budget-batch execution."""
4
5
+import itertools
6
+import math
7
+from collections.abc import Hashable
8
from dataclasses import dataclass
6
-from typing import Any
9
+from typing import Any, TypeAlias
10
11
import torch
12
@@ -22,12 +25,20 @@ from vllm.model_executor.models.vision import get_load_balance_assignment
25
from vllm.utils.gpu_sync_debug import gpu_sync_allowed
26
from vllm.utils.torch_utils import current_stream
27
from vllm.v1.worker.encoder_cudagraph_defs import (
28
+ ENCODER_CUDAGRAPH_AXIS_KEYS_KWARG,
29
EncoderCudaGraphConfig,
30
EncoderItemSpec,
31
)
32
33
logger = init_logger(__name__)
34
35
+CaptureAxisKeys: TypeAlias = tuple[Hashable, ...]
36
+
37
+BudgetGraphMapKey: TypeAlias = int | tuple[int, CaptureAxisKeys]
38
+
39
+# Warn when capture axes multiply the captured graph count past this point.
40
+_CAPTURE_GRAPH_COUNT_WARNING_THRESHOLD = 64
41
+
42
43
@dataclass
44
class BudgetGraphMetadata:
@@ -50,6 +61,8 @@ class BudgetGraphMetadata:
61
input_buffers: dict[str, torch.Tensor]
62
# Output written by graph, read after replay
63
output_buffer: torch.Tensor
64
+ # Per-axis capture keys, empty when no extra capture axis is used.
65
+ axis_keys: CaptureAxisKeys = ()
66
67
68
class EncoderCudaGraphManager:
@@ -77,6 +90,18 @@ class EncoderCudaGraphManager:
90
91
multimodal_config = vllm_config.model_config.multimodal_config
92
93
+ self._capture_axes: tuple[CaptureAxisKeys, ...] = tuple(
94
+ tuple(axis) for axis in self.config.capture_axes
95
+ )
96
+ if any(len(axis) == 0 for axis in self._capture_axes):
97
+ raise ValueError("Encoder cudagraph capture axes must be non-empty.")
98
+
99
+ if len(self.config.paths) > 1 and self._capture_axes:
100
+ raise NotImplementedError(
101
+ "Combining dual-path encoder CUDA graphs with capture axes "
102
+ "is not supported yet."
103
+ )
104
+
105
# Invariant: max_batch_size <= min_token_budget.
106
# This ensures per_image_output = budget // max_batch_size >= 1
107
# for every captured budget, preventing reshape crashes on empty
@@ -154,7 +179,7 @@ class EncoderCudaGraphManager:
179
and vllm_config.parallel_config.tensor_parallel_size > 1
180
)
181
157
- self.budget_graphs: dict[str, dict[int, BudgetGraphMetadata]] = {}
182
+ self.budget_graphs: dict[str, dict[BudgetGraphMapKey, BudgetGraphMetadata]] = {}
183
self.graph_pool: Any | None = None
184
self.graph_hits = 0
185
self.graph_misses = 0
@@ -181,11 +206,13 @@ class EncoderCudaGraphManager:
206
207
logger.info(
208
"EncoderCudaGraphManager initialized with paths=%s, "
184
- "max_batch_size=%d, max_frames_per_batch=%s, use_dp=%s",
209
+ "max_batch_size=%d, max_frames_per_batch=%s, use_dp=%s, "
210
+ "capture_axes=%s",
211
self.path_token_budgets,
212
self.max_batch_size,
213
self.max_frames_per_batch,
214
self.use_dp,
215
+ self._capture_axes,
216
)
217
218
@staticmethod
@@ -219,38 +246,63 @@ class EncoderCudaGraphManager:
246
"""Capture CUDA graphs for every configured path and token budget."""
247
self.graph_pool = graph_pool
248
249
+ num_graphs = self.get_num_graphs_to_capture()
250
+ if num_graphs > _CAPTURE_GRAPH_COUNT_WARNING_THRESHOLD:
251
+ logger.warning(
252
+ "Capturing %d encoder CUDA graphs; capture axes multiply the "
253
+ "per-budget graph count, consider pruning axis keys.",
254
+ num_graphs,
255
+ )
256
+
257
for path, budgets in self.path_token_budgets.items():
258
for token_budget in sorted(budgets, reverse=True):
224
- if token_budget > 0:
225
- self._capture_budget_graph(token_budget, path=path)
259
+ if token_budget == 0:
260
+ continue
261
+ for axis_keys in itertools.product(*self._capture_axes):
262
+ self._capture_budget_graph(
263
+ token_budget,
264
+ path=path,
265
+ axis_keys=axis_keys,
266
+ )
267
268
logger.info(
269
"Encoder CUDA graph capture complete. Captured %d graphs across %d paths.",
229
- self.get_num_graphs_to_capture(),
270
+ num_graphs,
271
len(self.path_token_budgets),
272
)
273
274
+ def _num_graphs_for_budgets(self, budgets: list[int]) -> int:
275
+ num_budgets = sum(1 for budget in budgets if budget != 0)
276
+ return num_budgets * math.prod(len(axis) for axis in self._capture_axes)
277
+
278
def get_num_graphs_to_capture(self) -> int:
279
return sum(
235
- token_budget > 0
280
+ self._num_graphs_for_budgets(budgets)
281
for budgets in self.path_token_budgets.values()
237
- for token_budget in budgets
282
)
283
240
- def _get_graph_set(self, path: str = "default") -> dict[int, BudgetGraphMetadata]:
284
+ def _get_graph_set(
285
+ self, path: str = "default"
286
+ ) -> dict[BudgetGraphMapKey, BudgetGraphMetadata]:
287
"""Return the captured graphs for one encoder path."""
288
if path not in self.budget_graphs:
289
self.budget_graphs[path] = {}
290
return self.budget_graphs[path]
291
246
- def _capture_budget_graph(self, token_budget: int, path: str = "default"):
292
+ def _capture_budget_graph(
293
+ self,
294
+ token_budget: int,
295
+ path: str = "default",
296
+ axis_keys: CaptureAxisKeys = (),
297
+ ):
298
"""Capture CUDA graph for a single token budget."""
299
logger.debug(
300
"Capturing encoder cudagraph for budget=%d, max_batch_size=%d, "
250
- "max_frames_per_batch=%d",
301
+ "max_frames_per_batch=%d, axis_keys=%s",
302
token_budget,
303
self.max_batch_size,
304
self.max_frames_per_batch,
305
+ axis_keys,
306
)
307
308
graph_set = self._get_graph_set(path)
@@ -262,6 +314,7 @@ class EncoderCudaGraphManager:
314
self.device,
315
self.dtype,
316
path,
317
+ axis_keys,
318
)
319
320
values = capture_inputs.values
@@ -278,13 +331,17 @@ class EncoderCudaGraphManager:
331
output = self.model.encoder_cudagraph_forward({**values}, path=path)
332
output_buffer.copy_(output)
333
281
- graph_set[token_budget] = BudgetGraphMetadata(
334
+ graph_map_key: BudgetGraphMapKey = (
335
+ token_budget if not axis_keys else (token_budget, axis_keys)
336
+ )
337
+ graph_set[graph_map_key] = BudgetGraphMetadata(
338
token_budget=token_budget,
339
max_batch_size=self.max_batch_size,
340
max_frames_per_batch=self.max_frames_per_batch,
341
graph=graph,
342
input_buffers=values,
343
output_buffer=output_buffer,
344
+ axis_keys=axis_keys,
345
)
346
347
def _find_smallest_fitting_budget_given_tokens(
@@ -310,12 +367,24 @@ class EncoderCudaGraphManager:
367
368
def _select_items(
369
self, mm_kwargs: dict[str, Any], indices: list[int]
313
- ) -> dict[str, Any]:
314
- """Select the mm kwargs for `indices` from the model."""
370
+ ) -> tuple[dict[str, Any], CaptureAxisKeys]:
371
+ """Select the mm kwargs for `indices` from the model.
372
+
373
+ Returns the sliced mm_kwargs together with the resolved capture-axis
374
+ keys (popped out of the returned dict; empty without capture axes).
375
+ """
376
# Same as `_get_item_specs`: implementations re-read the per-item
377
# grid/patch counts to slice the batch, so the D2H is inherent.
378
with gpu_sync_allowed():
318
- return self.model.select_encoder_cudagraph_items(mm_kwargs, indices)
379
+ selected = self.model.select_encoder_cudagraph_items(mm_kwargs, indices)
380
+ axis_keys = selected.pop(ENCODER_CUDAGRAPH_AXIS_KEYS_KWARG, ())
381
+ if len(axis_keys) != len(self._capture_axes):
382
+ raise ValueError(
383
+ f"Model returned {len(axis_keys)} capture axis keys, "
384
+ f"expected {len(self._capture_axes)} "
385
+ f"(one per axis of capture_axes)."
386
+ )
387
+ return selected, axis_keys
388
389
def _get_per_item_out_tokens(self, mm_kwargs: dict[str, Any]) -> list[int]:
390
"""Get per-item output token counts as plain ints."""
@@ -334,6 +403,7 @@ class EncoderCudaGraphManager:
403
mm_kwargs: dict[str, Any],
404
token_budget: int,
405
path: str = "default",
406
+ axis_keys: CaptureAxisKeys = (),
407
) -> torch.Tensor | None:
408
"""Execute budget graph.
409
@@ -347,11 +417,14 @@ class EncoderCudaGraphManager:
417
graph_set = self._get_graph_set(path)
418
num_items = len(self._get_item_specs(mm_kwargs))
419
350
- if token_budget not in graph_set:
420
+ graph_map_key: BudgetGraphMapKey = (
421
+ token_budget if not axis_keys else (token_budget, axis_keys)
422
+ )
423
+ if graph_map_key not in graph_set:
424
self.graph_misses += num_items
425
return None
426
354
- graph_meta = graph_set[token_budget]
427
+ graph_meta = graph_set[graph_map_key]
428
429
replay = self.model.prepare_encoder_cudagraph_replay_buffers(
430
mm_kwargs,
@@ -432,7 +505,7 @@ class EncoderCudaGraphManager:
505
506
outputs_by_orig_idx: dict[int, torch.Tensor] = {}
507
for batch_indices, path_budgets in batches:
435
- batch_mm_kwargs = self._select_items(mm_kwargs, batch_indices)
508
+ batch_mm_kwargs, axis_keys = self._select_items(mm_kwargs, batch_indices)
509
graph_outputs: dict[str, torch.Tensor] = {}
510
all_eager = True
511
@@ -449,10 +522,14 @@ class EncoderCudaGraphManager:
522
)
523
else:
524
all_eager = False
452
- output = self._run_budget_graph(
453
- batch_mm_kwargs, token_budget, path=path
525
+ graph_output = self._run_budget_graph(
526
+ batch_mm_kwargs,
527
+ token_budget,
528
+ path=path,
529
+ axis_keys=axis_keys,
530
)
455
- assert output is not None
531
+ assert graph_output is not None
532
+ output = graph_output
533
graph_outputs[path] = output
534
535
if all_eager:
@@ -506,9 +583,9 @@ class EncoderCudaGraphManager:
583
]
584
585
if len(local_indices) > 0:
509
- local_mm_kwargs = self._select_items(mm_kwargs, local_indices)
586
+ local_mm_kwargs = self._select_items(mm_kwargs, local_indices)[0]
587
else:
511
- local_mm_kwargs = self._select_items(mm_kwargs, [])
588
+ local_mm_kwargs = self._select_items(mm_kwargs, [])[0]
589
590
max_output_tokens_per_rank = (
591
max(
vllm/v1/worker/encoder_cudagraph_defs.py
+15
-1
@@ -2,13 +2,19 @@
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
"""Data transfer objects for encoder CUDA graph management."""
4
5
-from collections.abc import Callable
5
+from collections.abc import Callable, Hashable
6
from dataclasses import dataclass, field
7
8
import torch
9
10
EncoderCudaGraphPaddingLogic = Callable[[torch.Tensor, torch.Tensor], None]
11
12
+# Reserved mm_kwargs key: models with ``EncoderCudaGraphConfig.capture_axes``
13
+# put the resolved per-axis keys (one per axis, in order) into the dict
14
+# returned by ``select_encoder_cudagraph_items()``; the manager pops it before
15
+# the kwargs are used anywhere else.
16
+ENCODER_CUDAGRAPH_AXIS_KEYS_KWARG = "encoder_cudagraph_axis_keys"
17
+
18
19
@dataclass
20
class EncoderItemSpec:
@@ -88,6 +94,14 @@ class EncoderCudaGraphConfig:
94
)
95
"""Independently captured encoder paths keyed by their forward name."""
96
97
+ capture_axes: tuple[tuple[Hashable, ...], ...] = ()
98
+ """Extra capture axes beyond the token budget; empty to disable.
99
+
100
+ Each entry is the ordered key set of one axis. When non-empty, one graph
101
+ is captured per token budget per combination of axis keys (cartesian
102
+ product), so the total number of captured graphs is
103
+ ``num_budgets * prod(len(axis) for axis in capture_axes)``."""
104
+
105
106
@dataclass
107
class EncoderCudaGraphCaptureInputs: