A high-throughput and memory-efficient inference and serving engine for LLMs
Mirrored from https://github.com/tduncan-lum/Nvidia-Optimization-Stack---VLLM.git
Synced Nov 7, 15:15 UTC
Flashinfer cuDNN backend for Qwen3 VL ViT attention (#34580)
Signed-off-by: Max Hu <maxhu@nvidia.com> Signed-off-by: Max Hu <hyoung2991@gmail.com> Co-authored-by: Max Hu <maxhu@nvidia.com> Co-authored-by: Shang Wang <shangw@nvidia.com>
Max Hu committed
Feb 27, 2026 at 20:20 UTC
9c3fe9936b929b5503d780bd4e8e3cd524de1c4e
6 files changed
+405
-21
tests/kernels/attention/test_mha_attn.py
+110
@@ -9,9 +9,12 @@ Test:
9
import itertools
10
from unittest.mock import patch
11
12
+import numpy as np
13
import pytest
14
import torch
15
16
+from vllm.config import get_current_vllm_config
17
+from vllm.config.multimodal import MultiModalConfig
18
from vllm.model_executor.layers.attention import MMEncoderAttention
19
from vllm.platforms import current_platform
20
from vllm.platforms.cpu import CpuPlatform
@@ -224,3 +227,110 @@ def test_mha_attn_varlen_forward(
227
ref_output.append(output_i)
228
ref_output = torch.cat(ref_output, dim=1)
229
torch.testing.assert_close(output, ref_output, atol=1e-2, rtol=1e-2)
230
+
231
+
232
+@pytest.mark.parametrize("var_seq_len", VAR_SEQ_LENS)
233
+@pytest.mark.parametrize(
234
+ "dtype",
235
+ [torch.bfloat16, torch.half],
236
+)
237
+@pytest.mark.parametrize("device", CUDA_DEVICES)
238
+def test_mha_attn_varlen_forward_flashinfer(
239
+ default_vllm_config,
240
+ var_seq_len: list[int],
241
+ dtype: torch.dtype,
242
+ device: str,
243
+):
244
+ """Test MMEncoderAttention varlen forward with FLASHINFER backend (head_size=72).
245
+
246
+ Exercises the path that uses --mm-encoder-attn-backend=FLASHINFER with
247
+ recomputed cu_seqlens, max_seqlen, and sequence_lengths as in qwen3_vl
248
+ vision encoder.
249
+ """
250
+ pytest.importorskip("flashinfer")
251
+
252
+ num_heads = 16
253
+ head_size = 72
254
+ set_random_seed(0)
255
+ torch.set_default_device(device)
256
+ torch.set_default_dtype(dtype)
257
+
258
+ # Override vllm config so get_vit_attn_backend returns FLASHINFER (simulates
259
+ # --mm-encoder-attn-backend=FLASHINFER).
260
+ vllm_config = get_current_vllm_config()
261
+ old_model_config = getattr(vllm_config, "model_config", None)
262
+ minimal_model_config = type(
263
+ "MinimalModelConfig",
264
+ (),
265
+ {
266
+ "multimodal_config": MultiModalConfig(
267
+ mm_encoder_attn_backend=AttentionBackendEnum.FLASHINFER
268
+ ),
269
+ },
270
+ )()
271
+ vllm_config.model_config = minimal_model_config
272
+ try:
273
+ total_len = sum(var_seq_len)
274
+ # Stride of second dim = 3 * num_heads * head_size (same as qwen2_5_vl
275
+ # after qkv rearrange and unbind: qkv shape (b, s, 3, head, head_dim)).
276
+ qkv = torch.randn(1, total_len, 3, num_heads, head_size)
277
+ q, k, v = qkv.unbind(dim=2)
278
+
279
+ cu_seqlens_np = np.array(
280
+ [0] + list(itertools.accumulate(var_seq_len)), dtype=np.int32
281
+ )
282
+ hidden_size = num_heads * head_size
283
+ tp_size = 1
284
+
285
+ sequence_lengths_np = MMEncoderAttention.maybe_compute_sequence_lengths(
286
+ AttentionBackendEnum.FLASHINFER, cu_seqlens_np
287
+ )
288
+ sequence_lengths = torch.from_numpy(sequence_lengths_np).to(
289
+ device, dtype=torch.int32, non_blocking=True
290
+ )
291
+
292
+ max_seqlen_val = MMEncoderAttention.compute_max_seqlen(
293
+ AttentionBackendEnum.FLASHINFER, cu_seqlens_np
294
+ )
295
+ max_seqlen = torch.tensor(max_seqlen_val, device=device, dtype=torch.int32)
296
+
297
+ cu_seqlens_np = MMEncoderAttention.maybe_recompute_cu_seqlens(
298
+ AttentionBackendEnum.FLASHINFER,
299
+ cu_seqlens_np,
300
+ hidden_size,
301
+ tp_size,
302
+ )
303
+ cu_seqlens = torch.from_numpy(cu_seqlens_np).to(
304
+ device, dtype=torch.int32, non_blocking=True
305
+ )
306
+
307
+ scale = 1.0 / head_size**0.5
308
+ attn = MMEncoderAttention(
309
+ num_heads,
310
+ head_size,
311
+ scale=scale,
312
+ num_kv_heads=num_heads,
313
+ )
314
+ assert attn.attn_backend == AttentionBackendEnum.FLASHINFER
315
+
316
+ output = attn(
317
+ q,
318
+ k,
319
+ v,
320
+ cu_seqlens=cu_seqlens,
321
+ max_seqlen=max_seqlen,
322
+ sequence_lengths=sequence_lengths,
323
+ )
324
+
325
+ ref_output = []
326
+ for q_i, k_i, v_i in zip(
327
+ torch.split(q, var_seq_len, dim=1),
328
+ torch.split(k, var_seq_len, dim=1),
329
+ torch.split(v, var_seq_len, dim=1),
330
+ ):
331
+ output_i = ref_attention(q_i, k_i, v_i, scale=scale)
332
+ ref_output.append(output_i)
333
+ ref_output = torch.cat(ref_output, dim=1)
334
+ torch.testing.assert_close(output, ref_output, atol=1e-2, rtol=1e-2)
335
+ finally:
336
+ vllm_config.model_config = old_model_config
vllm/model_executor/layers/attention/mm_encoder_attention.py
+170
-2
@@ -2,21 +2,93 @@
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4
5
+import numpy as np
6
import torch
7
8
from vllm.logger import init_logger
9
from vllm.model_executor.custom_op import CustomOp
10
from vllm.model_executor.models.vision import get_vit_attn_backend
11
+from vllm.utils.math_utils import round_up
12
from vllm.v1.attention.backends.fa_utils import get_flash_attn_version
13
from vllm.v1.attention.backends.registry import AttentionBackendEnum
14
from vllm.v1.attention.ops.vit_attn_wrappers import (
15
vit_flash_attn_wrapper,
16
+ vit_flashinfer_wrapper,
17
vit_torch_sdpa_wrapper,
18
vit_triton_attn_wrapper,
19
)
20
21
logger = init_logger(__name__)
22
23
+# Batch buckets for cuDNN graph caching.
24
+# Graphs use batch size and max sequence length as cache key.
25
+# This avoids creating a new graph for each unique set of
26
+# batch size and max sequence length at runtime.
27
+# From the cuDNN team's performance measurements, there
28
+# is no significant kernel performance difference between padding
29
+# to a smaller batch size/seq length and padding to larger
30
+# ones. The bucketing here is solely used to avoid memory
31
+# operation overhead, which won't be needed if we have CUDA
32
+# graph support in the future.
33
+# TODO: Remove buckets after issue #34763
34
+# (cuda graph support) is addressed.
35
+FLASHINFER_BATCH_BUCKETS = [8, 16, 32, 64]
36
+FLASHINFER_MAX_SEQLEN_BUCKETS = [
37
+ 1 * 1024,
38
+ 2 * 1024,
39
+ 4 * 1024,
40
+ 8 * 1024,
41
+ 16 * 1024,
42
+ 32 * 1024,
43
+ 64 * 1024,
44
+ 128 * 1024,
45
+]
46
+
47
+# Workspace buffer for FlashInfer CuDNN backend
48
+FLASHINFER_CUDNN_WORKSPACE_SIZE_BYTES = 128 * 1024 * 1024
49
+_flashinfer_workspace_buffer: torch.Tensor | None = None
50
+
51
+
52
+def _get_flashinfer_workspace_buffer() -> torch.Tensor:
53
+ global _flashinfer_workspace_buffer
54
+ if _flashinfer_workspace_buffer is None:
55
+ _flashinfer_workspace_buffer = torch.zeros(
56
+ FLASHINFER_CUDNN_WORKSPACE_SIZE_BYTES,
57
+ dtype=torch.uint8,
58
+ device="cuda",
59
+ )
60
+ return _flashinfer_workspace_buffer
61
+
62
+
63
+def add_padding_to_seqlens(
64
+ seq: np.ndarray,
65
+ batch_size: int,
66
+ padding_value: int,
67
+) -> np.ndarray:
68
+ batch_size_padded = next(
69
+ (b for b in FLASHINFER_BATCH_BUCKETS if b >= batch_size),
70
+ round_up(batch_size, FLASHINFER_BATCH_BUCKETS[0]),
71
+ )
72
+ if batch_size_padded == batch_size:
73
+ return seq
74
+ return np.concatenate(
75
+ [
76
+ seq,
77
+ np.full((batch_size_padded - batch_size,), padding_value, dtype=seq.dtype),
78
+ ]
79
+ )
80
+
81
+
82
+def bucket_flashinfer_max_seqlen(
83
+ real_max_seqlen: int,
84
+) -> int:
85
+ if real_max_seqlen <= 0:
86
+ return FLASHINFER_MAX_SEQLEN_BUCKETS[0]
87
+ return next(
88
+ (s for s in FLASHINFER_MAX_SEQLEN_BUCKETS if s >= real_max_seqlen),
89
+ round_up(real_max_seqlen, FLASHINFER_MAX_SEQLEN_BUCKETS[-1]),
90
+ )
91
+
92
93
# --8<-- [start:mm_encoder_attn]
94
@CustomOp.register("mm_encoder_attn")
@@ -24,6 +96,67 @@ class MMEncoderAttention(CustomOp):
96
"""Multi-headed attention without any cache, used for multimodal encoder."""
97
98
# --8<-- [end:mm_encoder_attn]
99
+ @classmethod
100
+ def compute_max_seqlen(
101
+ cls,
102
+ attn_backend: AttentionBackendEnum,
103
+ cu_seqlens: np.ndarray,
104
+ ) -> int:
105
+ max_seqlen = 0
106
+ if (
107
+ attn_backend
108
+ in (
109
+ AttentionBackendEnum.FLASH_ATTN,
110
+ AttentionBackendEnum.ROCM_AITER_FA,
111
+ AttentionBackendEnum.TRITON_ATTN,
112
+ AttentionBackendEnum.FLASHINFER,
113
+ )
114
+ and len(cu_seqlens) >= 2
115
+ ):
116
+ max_seqlen = int((cu_seqlens[1:] - cu_seqlens[:-1]).max())
117
+ if attn_backend == AttentionBackendEnum.FLASHINFER:
118
+ max_seqlen = bucket_flashinfer_max_seqlen(max_seqlen)
119
+ return max_seqlen
120
+
121
+ @classmethod
122
+ def maybe_compute_sequence_lengths(
123
+ cls,
124
+ attn_backend: AttentionBackendEnum,
125
+ cu_seqlens: np.ndarray,
126
+ ) -> np.ndarray | None:
127
+ if attn_backend != AttentionBackendEnum.FLASHINFER:
128
+ return None
129
+ sequence_lengths = cu_seqlens[1:] - cu_seqlens[:-1]
130
+ sequence_lengths = add_padding_to_seqlens(
131
+ sequence_lengths, len(sequence_lengths), 0
132
+ )
133
+ return sequence_lengths
134
+
135
+ @classmethod
136
+ def maybe_recompute_cu_seqlens(
137
+ cls,
138
+ attn_backend: AttentionBackendEnum,
139
+ cu_seqlens: np.ndarray,
140
+ hidden_size: int,
141
+ tp_size: int,
142
+ ) -> np.ndarray:
143
+ if attn_backend != AttentionBackendEnum.FLASHINFER:
144
+ return cu_seqlens
145
+
146
+ batch_size = len(cu_seqlens) - 1
147
+ scale = hidden_size // tp_size
148
+ cu_seqlens = cu_seqlens * scale
149
+
150
+ cu_seqlens_qko = cu_seqlens
151
+ cu_seqlens_v = cu_seqlens * 3
152
+
153
+ cu_seqlens_qko = add_padding_to_seqlens(
154
+ cu_seqlens_qko, batch_size, cu_seqlens_qko[-1]
155
+ )
156
+ cu_seqlens_v = add_padding_to_seqlens(
157
+ cu_seqlens_v, batch_size, cu_seqlens_v[-1]
158
+ )
159
+ return np.concatenate([cu_seqlens_qko, cu_seqlens_v])
160
161
def __init__(
162
self,
@@ -46,10 +179,9 @@ class MMEncoderAttention(CustomOp):
179
180
self.num_heads = num_heads
181
self.head_size = head_size
49
- self.scale = scale
182
+ self.scale = 1.0 / (head_size**0.5) if scale is None else scale
183
self.num_kv_heads = num_heads if num_kv_heads is None else num_kv_heads
184
self.layer_name = prefix
52
-
185
assert self.num_heads % self.num_kv_heads == 0, (
186
f"num_heads ({self.num_heads}) is not "
187
f"divisible by num_kv_heads ({self.num_kv_heads})"
@@ -75,6 +207,9 @@ class MMEncoderAttention(CustomOp):
207
get_flash_attn_version() if self.is_flash_attn_backend else None
208
)
209
210
+ if self.attn_backend == AttentionBackendEnum.FLASHINFER:
211
+ _get_flashinfer_workspace_buffer()
212
+
213
logger.info_once(f"Using {self.attn_backend} for MMEncoderAttention.")
214
215
@classmethod
@@ -201,6 +336,27 @@ class MMEncoderAttention(CustomOp):
336
output = output.reshape(bsz, q_len, -1)
337
return output
338
339
+ def _forward_flashinfer(
340
+ self,
341
+ query: torch.Tensor,
342
+ key: torch.Tensor,
343
+ value: torch.Tensor,
344
+ cu_seqlens: torch.Tensor | None = None,
345
+ max_seqlen: torch.Tensor | None = None,
346
+ sequence_lengths: torch.Tensor
347
+ | None = None, # Only used for FlashInfer CuDNN backend
348
+ ) -> torch.Tensor:
349
+ return vit_flashinfer_wrapper(
350
+ q=query,
351
+ k=key,
352
+ v=value,
353
+ scale=self.scale,
354
+ workspace_buffer=_get_flashinfer_workspace_buffer(),
355
+ cu_seqlens=cu_seqlens,
356
+ max_seqlen=max_seqlen,
357
+ sequence_lengths=sequence_lengths,
358
+ )
359
+
360
def forward_native(
361
self,
362
query: torch.Tensor,
@@ -208,6 +364,8 @@ class MMEncoderAttention(CustomOp):
364
value: torch.Tensor,
365
cu_seqlens: torch.Tensor | None = None,
366
max_seqlen: torch.Tensor | None = None, # Only used for Flash Attention
367
+ sequence_lengths: torch.Tensor
368
+ | None = None, # Only used for FlashInfer CuDNN backend
369
) -> torch.Tensor:
370
return self._forward_sdpa(query, key, value, cu_seqlens)
371
@@ -218,11 +376,17 @@ class MMEncoderAttention(CustomOp):
376
value: torch.Tensor,
377
cu_seqlens: torch.Tensor | None = None,
378
max_seqlen: torch.Tensor | None = None, # Only used for Flash Attention
379
+ sequence_lengths: torch.Tensor
380
+ | None = None, # Only used for FlashInfer CuDNN backend
381
) -> torch.Tensor:
382
if self.is_flash_attn_backend:
383
return self._forward_fa(query, key, value, cu_seqlens, max_seqlen)
384
elif self.attn_backend == AttentionBackendEnum.TRITON_ATTN:
385
return self._forward_triton(query, key, value, cu_seqlens, max_seqlen)
386
+ elif self.attn_backend == AttentionBackendEnum.FLASHINFER:
387
+ return self._forward_flashinfer(
388
+ query, key, value, cu_seqlens, max_seqlen, sequence_lengths
389
+ )
390
elif self.attn_backend == AttentionBackendEnum.TORCH_SDPA:
391
return self._forward_sdpa(query, key, value, cu_seqlens)
392
else:
@@ -238,6 +402,8 @@ class MMEncoderAttention(CustomOp):
402
value: torch.Tensor,
403
cu_seqlens: torch.Tensor | None = None,
404
max_seqlen: torch.Tensor | None = None, # Only used for Flash Attention
405
+ sequence_lengths: torch.Tensor
406
+ | None = None, # Only used for FlashInfer CuDNN backend
407
) -> torch.Tensor:
408
return self._forward_sdpa(query, key, value, cu_seqlens)
409
@@ -248,6 +414,8 @@ class MMEncoderAttention(CustomOp):
414
value: torch.Tensor,
415
cu_seqlens: torch.Tensor | None = None,
416
max_seqlen: torch.Tensor | None = None, # Only used for Flash Attention
417
+ sequence_lengths: torch.Tensor
418
+ | None = None, # Only used for FlashInfer CuDNN backend
419
) -> torch.Tensor:
420
if self.attn_backend == AttentionBackendEnum.FLASH_ATTN:
421
return self._forward_fa(query, key, value, cu_seqlens, max_seqlen)
vllm/model_executor/models/qwen2_5_vl.py
+3
@@ -357,6 +357,7 @@ class Qwen2_5_VisionAttention(nn.Module):
357
rotary_pos_emb_cos: torch.Tensor,
358
rotary_pos_emb_sin: torch.Tensor,
359
max_seqlen: torch.Tensor, # Only used for Flash Attention
360
+ sequence_lengths: torch.Tensor, # Only used for FlashInfer CuDNN backend
361
) -> torch.Tensor:
362
# [s, b, c] --> [s, b, head * 3 * head_dim]
363
x, _ = self.qkv(x)
@@ -398,6 +399,7 @@ class Qwen2_5_VisionAttention(nn.Module):
399
value=v,
400
cu_seqlens=cu_seqlens,
401
max_seqlen=max_seqlen,
402
+ sequence_lengths=sequence_lengths,
403
)
404
405
context_layer = einops.rearrange(
@@ -463,6 +465,7 @@ class Qwen2_5_VisionBlock(nn.Module):
465
rotary_pos_emb_cos=rotary_pos_emb_cos,
466
rotary_pos_emb_sin=rotary_pos_emb_sin,
467
max_seqlen=max_seqlen,
468
+ sequence_lengths=None,
469
)
470
x_fused_norm, residual = self.norm2(x, residual=x_attn)
471
x = residual + self.mlp(x_fused_norm)
vllm/model_executor/models/qwen3_vl.py
+33
-19
@@ -51,9 +51,12 @@ from transformers.video_utils import VideoMetadata
51
from vllm.compilation.decorators import support_torch_compile
52
from vllm.config import VllmConfig
53
from vllm.config.multimodal import BaseDummyOptions, VideoDummyOptions
54
-from vllm.distributed import get_pp_group
54
+from vllm.distributed import get_pp_group, parallel_state
55
from vllm.logger import init_logger
56
from vllm.model_executor.layers.activation import _ACTIVATION_REGISTRY
57
+from vllm.model_executor.layers.attention.mm_encoder_attention import (
58
+ MMEncoderAttention,
59
+)
60
from vllm.model_executor.layers.conv import Conv3dLayer
61
from vllm.model_executor.layers.linear import (
62
ColumnParallelLinear,
@@ -92,7 +95,6 @@ from vllm.multimodal.processing import (
95
from vllm.sequence import IntermediateTensors
96
from vllm.utils.collection_utils import is_list_of
97
from vllm.utils.math_utils import round_up
95
-from vllm.v1.attention.backends.registry import AttentionBackendEnum
98
99
from .interfaces import (
100
MultiModalEmbeddings,
@@ -244,6 +246,7 @@ class Qwen3_VisionBlock(nn.Module):
246
rotary_pos_emb_cos: torch.Tensor,
247
rotary_pos_emb_sin: torch.Tensor,
248
max_seqlen: torch.Tensor, # Only used for Flash Attention
249
+ sequence_lengths: torch.Tensor, # Only used for FlashInfer CuDNN backend
250
) -> torch.Tensor:
251
x = x + self.attn(
252
self.norm1(x),
@@ -251,6 +254,7 @@ class Qwen3_VisionBlock(nn.Module):
254
rotary_pos_emb_cos=rotary_pos_emb_cos,
255
rotary_pos_emb_sin=rotary_pos_emb_sin,
256
max_seqlen=max_seqlen,
257
+ sequence_lengths=sequence_lengths,
258
)
259
260
x = x + self.mlp(self.norm2(x))
@@ -332,6 +336,13 @@ class Qwen3_VisionTransformer(nn.Module):
336
)
337
self.num_grid_per_side = int(self.num_position_embeddings**0.5)
338
339
+ use_data_parallel = is_vit_use_data_parallel()
340
+ self.tp_size = (
341
+ 1
342
+ if use_data_parallel
343
+ else parallel_state.get_tensor_model_parallel_world_size()
344
+ )
345
+
346
# NOTE: This is used for creating empty tensor for all_gather for
347
# DP ViT. Here out_hidden_size is enlarged due to deepstack
348
self.out_hidden_size = vision_config.out_hidden_size * (
@@ -513,19 +524,6 @@ class Qwen3_VisionTransformer(nn.Module):
524
525
return torch.cat(outputs, dim=0)
526
516
- def compute_attn_mask_seqlen(
517
- self,
518
- cu_seqlens: torch.Tensor,
519
- ) -> torch.Tensor:
520
- max_seqlen = torch.zeros([], device=cu_seqlens.device)
521
- if self.attn_backend in (
522
- AttentionBackendEnum.FLASH_ATTN,
523
- AttentionBackendEnum.ROCM_AITER_FA,
524
- AttentionBackendEnum.TRITON_ATTN,
525
- ):
526
- max_seqlen = (cu_seqlens[1:] - cu_seqlens[:-1]).max()
527
- return max_seqlen
528
-
527
def forward(
528
self,
529
x: torch.Tensor,
@@ -549,11 +547,26 @@ class Qwen3_VisionTransformer(nn.Module):
547
axis=0, dtype=np.int32
548
)
549
cu_seqlens = np.concatenate([np.zeros(1, dtype=np.int32), cu_seqlens])
552
- cu_seqlens = torch.from_numpy(cu_seqlens)
553
-
550
+ sequence_lengths = MMEncoderAttention.maybe_compute_sequence_lengths(
551
+ self.attn_backend, cu_seqlens
552
+ )
553
+ if sequence_lengths is not None:
554
+ sequence_lengths = torch.from_numpy(sequence_lengths).to(
555
+ self.device, non_blocking=True
556
+ )
557
+ max_seqlen = torch.tensor(
558
+ MMEncoderAttention.compute_max_seqlen(self.attn_backend, cu_seqlens),
559
+ dtype=torch.int32,
560
+ device=self.device,
561
+ )
562
+ cu_seqlens = MMEncoderAttention.maybe_recompute_cu_seqlens(
563
+ self.attn_backend,
564
+ cu_seqlens,
565
+ self.hidden_size,
566
+ self.tp_size,
567
+ )
568
+ cu_seqlens = torch.from_numpy(cu_seqlens).to(self.device, non_blocking=True)
569
hidden_states = hidden_states.unsqueeze(1)
555
- max_seqlen = self.compute_attn_mask_seqlen(cu_seqlens)
556
- cu_seqlens = cu_seqlens.to(self.device, non_blocking=True)
570
571
deepstack_feature_lists = []
572
for layer_num, blk in enumerate(self.blocks):
@@ -563,6 +576,7 @@ class Qwen3_VisionTransformer(nn.Module):
576
rotary_pos_emb_cos=rotary_pos_emb_cos,
577
rotary_pos_emb_sin=rotary_pos_emb_sin,
578
max_seqlen=max_seqlen,
579
+ sequence_lengths=sequence_lengths,
580
)
581
if layer_num in self.deepstack_visual_indexes:
582
deepstack_merger_idx = self.deepstack_visual_indexes.index(layer_num)
vllm/platforms/cuda.py
+1
@@ -414,6 +414,7 @@ class CudaPlatformBase(Platform):
414
AttentionBackendEnum.FLASH_ATTN,
415
AttentionBackendEnum.TRITON_ATTN,
416
AttentionBackendEnum.TORCH_SDPA,
417
+ AttentionBackendEnum.FLASHINFER,
418
]
419
420
@classmethod
vllm/v1/attention/ops/vit_attn_wrappers.py
+88
@@ -268,3 +268,91 @@ def vit_torch_sdpa_wrapper(
268
return torch.ops.vllm.torch_sdpa_wrapper(
269
q, k, v, scale, cu_seqlens, enable_gqa=enable_gqa
270
)
271
+
272
+
273
+def flashinfer_wrapper(
274
+ q: torch.Tensor,
275
+ k: torch.Tensor,
276
+ v: torch.Tensor,
277
+ scale: float,
278
+ workspace_buffer: torch.Tensor,
279
+ cu_seqlens: torch.Tensor | None = None,
280
+ max_seqlen: torch.Tensor | None = None,
281
+ sequence_lengths: torch.Tensor | None = None,
282
+) -> torch.Tensor:
283
+ from flashinfer.prefill import cudnn_batch_prefill_with_kv_cache
284
+
285
+ is_reshaped = q.dim() == 4
286
+
287
+ if is_reshaped:
288
+ reshape_batch_size = q.shape[0]
289
+ q, k, v = (einops.rearrange(x, "b s ... -> (b s) ...") for x in [q, k, v])
290
+ # cuDNN <= 9.10.2.21 requires q, k to be contiguous
291
+ # this comes with no cost for ViTs with RoPE because
292
+ # RoPE has already made q and k contiguous.
293
+ q, k = q.contiguous(), k.contiguous()
294
+
295
+ assert len(cu_seqlens) % 2 == 0, "cu_seqlens must be divisible by 2"
296
+ cu_seqlength = len(cu_seqlens) // 2
297
+ batch_offsets_qko = cu_seqlens[:cu_seqlength].view(-1, 1, 1, 1)
298
+ batch_offsets_v = cu_seqlens[cu_seqlength:].view(-1, 1, 1, 1)
299
+ sequence_lengths = sequence_lengths.view(-1, 1, 1, 1)
300
+ max_seqlen = max_seqlen.item()
301
+
302
+ output, _ = cudnn_batch_prefill_with_kv_cache(
303
+ q,
304
+ k,
305
+ v,
306
+ scale,
307
+ workspace_buffer,
308
+ max_token_per_sequence=max_seqlen,
309
+ max_sequence_kv=max_seqlen,
310
+ actual_seq_lens_q=sequence_lengths,
311
+ actual_seq_lens_kv=sequence_lengths,
312
+ causal=False,
313
+ return_lse=False,
314
+ batch_offsets_q=batch_offsets_qko,
315
+ batch_offsets_k=batch_offsets_qko,
316
+ batch_offsets_v=batch_offsets_v,
317
+ batch_offsets_o=batch_offsets_qko,
318
+ )
319
+
320
+ if is_reshaped:
321
+ output = einops.rearrange(output, "(b s) h d -> b s h d", b=reshape_batch_size)
322
+
323
+ return output
324
+
325
+
326
+def vit_flashinfer_wrapper_fake(
327
+ q: torch.Tensor,
328
+ k: torch.Tensor,
329
+ v: torch.Tensor,
330
+ scale: float,
331
+ workspace_buffer: torch.Tensor,
332
+ cu_seqlens: torch.Tensor | None = None,
333
+ max_seqlen: torch.Tensor | None = None,
334
+ sequence_lengths: torch.Tensor | None = None,
335
+) -> torch.Tensor:
336
+ return torch.empty_like(q)
337
+
338
+
339
+direct_register_custom_op(
340
+ op_name="flashinfer_wrapper",
341
+ op_func=flashinfer_wrapper,
342
+ fake_impl=vit_flashinfer_wrapper_fake,
343
+)
344
+
345
+
346
+def vit_flashinfer_wrapper(
347
+ q: torch.Tensor,
348
+ k: torch.Tensor,
349
+ v: torch.Tensor,
350
+ scale: float,
351
+ workspace_buffer: torch.Tensor,
352
+ cu_seqlens: torch.Tensor | None = None,
353
+ max_seqlen: torch.Tensor | None = None,
354
+ sequence_lengths: torch.Tensor | None = None,
355
+) -> torch.Tensor:
356
+ return torch.ops.vllm.flashinfer_wrapper(
357
+ q, k, v, scale, workspace_buffer, cu_seqlens, max_seqlen, sequence_lengths
358
+ )