[Model] Pixtral: use packed multimodal encoder attention (#52185)
Signed-off-by: Oliver Holworthy <1216955+oliverholworthy@users.noreply.github.com> Co-authored-by: OpenAI Codex <codex@openai.com>
Oliver Holworthy committed
Aug 26, 2026 at 15:14 UTC
28b484e5d741519feb72e5c0bf6ebc3b08176292
2 files changed
+157
-71
tests/models/multimodal/generation/test_pixtral.py
+34
@@ -5,6 +5,7 @@ from dataclasses import asdict
5
from typing import TYPE_CHECKING, Any
6
7
import pytest
8
+import torch
9
from mistral_common.multimodal import download_image
10
from mistral_common.protocol.instruct.chunk import ImageURLChunk
11
from mistral_common.protocol.instruct.request import ChatCompletionRequest
@@ -15,7 +16,9 @@ from transformers import AutoProcessor
16
from vllm import SamplingParams, TextPrompt, TokensPrompt
17
from vllm.inputs import MultiModalDataBuiltins
18
from vllm.logprobs import Logprob, SampleLogprobs
19
+from vllm.model_executor.models.pixtral import _make_packed_sequence_metadata
20
from vllm.platforms import current_platform
21
+from vllm.v1.attention.backends.registry import AttentionBackendEnum
22
23
from ....utils import VLLM_PATH, large_gpu_test
24
from ...utils import check_logprobs_close
@@ -123,6 +126,37 @@ FIXTURE_LOGPROBS_CHAT = {
126
OutputsLogprobs = list[tuple[list[int], str, SampleLogprobs | None]]
127
128
129
+@pytest.mark.parametrize(
130
+ "backend",
131
+ [
132
+ AttentionBackendEnum.FLASH_ATTN,
133
+ AttentionBackendEnum.FLASHINFER,
134
+ AttentionBackendEnum.TORCH_SDPA,
135
+ ],
136
+)
137
+def test_packed_sequence_metadata(backend: AttentionBackendEnum) -> None:
138
+ cu_seqlens, max_seqlen, sequence_lengths = _make_packed_sequence_metadata(
139
+ [4, 6],
140
+ backend,
141
+ hidden_size=64,
142
+ tp_size=1,
143
+ device=torch.device("cpu"),
144
+ )
145
+
146
+ assert cu_seqlens.dtype == torch.int32
147
+ assert max_seqlen.dtype == torch.int32
148
+ if backend == AttentionBackendEnum.FLASHINFER:
149
+ assert max_seqlen.item() >= 6
150
+ assert sequence_lengths is not None
151
+ assert sequence_lengths.dtype == torch.int32
152
+ assert len(cu_seqlens) % 2 == 0
153
+ else:
154
+ expected_max_seqlen = 6 if backend == AttentionBackendEnum.FLASH_ATTN else 0
155
+ assert max_seqlen.item() == expected_max_seqlen
156
+ assert cu_seqlens.tolist() == [0, 4, 10]
157
+ assert sequence_lengths is None
158
+
159
+
160
# For the test author to store golden output in JSON
161
def _dump_outputs_w_logprobs(
162
outputs: OutputsLogprobs,
vllm/model_executor/models/pixtral.py
+123
-71
@@ -6,6 +6,7 @@ from collections.abc import Iterable, Mapping, Sequence
6
from dataclasses import dataclass, fields
7
from typing import Annotated, Literal
8
9
+import numpy as np
10
import torch
11
import torch.nn as nn
12
from mistral_common.protocol.instruct.chunk import ImageChunk, TextChunk
@@ -26,6 +27,7 @@ from vllm.config.multimodal import BaseDummyOptions
27
from vllm.distributed import divide, get_tensor_model_parallel_world_size
28
from vllm.inputs import MultiModalDataDict
29
from vllm.model_executor.layers.activation import SiluAndMul, get_act_and_mul_fn
30
+from vllm.model_executor.layers.attention import MMEncoderAttention
31
from vllm.model_executor.layers.conv import Conv2dLayer
32
from vllm.model_executor.layers.layernorm import RMSNorm
33
from vllm.model_executor.layers.linear import (
@@ -59,7 +61,6 @@ from vllm.multimodal.processing.processor import (
61
PromptUpdate,
62
PromptUpdateDetails,
63
)
62
-from vllm.platforms import current_platform
64
from vllm.sequence import IntermediateTensors
65
from vllm.tokenizers import cached_tokenizer_from_config
66
from vllm.tokenizers.mistral import MistralTokenizer
@@ -69,6 +70,7 @@ from vllm.transformers_utils.processors.pixtral import (
70
)
71
from vllm.utils.collection_utils import is_list_of
72
from vllm.utils.tensor_schema import TensorSchema, TensorShape
73
+from vllm.v1.attention.backends.registry import AttentionBackendEnum
74
75
from .interfaces import (
76
MultiModalEmbeddings,
@@ -87,19 +89,35 @@ from .vision import (
89
resolve_visual_encoder_outputs,
90
)
91
90
-try:
91
- # Note: vLLM does not install xformers by default.
92
- from xformers import ops as xops
92
+PATCH_MERGE = "patch_merge"
93
94
- if current_platform.is_cuda() and current_platform.has_device_capability(100):
95
- # Xformers FA is not compatible with B200
96
- USE_XFORMERS_OPS = False
97
- else:
98
- USE_XFORMERS_OPS = True
99
-except ImportError:
100
- USE_XFORMERS_OPS = False
94
102
-PATCH_MERGE = "patch_merge"
95
+def _make_packed_sequence_metadata(
96
+ sequence_lengths: list[int],
97
+ attn_backend: AttentionBackendEnum,
98
+ hidden_size: int,
99
+ tp_size: int,
100
+ device: torch.device,
101
+) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor | None]:
102
+ lengths = np.array(sequence_lengths, dtype=np.int32)
103
+ cu_seqlens = np.concatenate(
104
+ [np.zeros(1, dtype=np.int32), lengths.cumsum(dtype=np.int32)]
105
+ )
106
+ sequence_lengths_tensor = MMEncoderAttention.maybe_compute_seq_lens(
107
+ attn_backend, cu_seqlens, device
108
+ )
109
+ max_seqlen = torch.tensor(
110
+ MMEncoderAttention.compute_max_seqlen(attn_backend, cu_seqlens),
111
+ dtype=torch.int32,
112
+ )
113
+ cu_seqlens_tensor = MMEncoderAttention.maybe_recompute_cu_seqlens(
114
+ attn_backend,
115
+ cu_seqlens,
116
+ hidden_size,
117
+ tp_size,
118
+ device,
119
+ )
120
+ return cu_seqlens_tensor, max_seqlen, sequence_lengths_tensor
121
122
123
def _is_layer_none_or_staged(layer: nn.Module) -> bool:
@@ -766,12 +784,19 @@ class Attention(nn.Module):
784
785
tp_size = 1 if disable_tp else get_tensor_model_parallel_world_size()
786
self.n_heads = divide(args.num_attention_heads, tp_size)
787
+ self.attn = MMEncoderAttention(
788
+ num_heads=self.n_heads,
789
+ head_size=self.head_dim,
790
+ prefix=f"{prefix}.attn",
791
+ )
792
793
def forward(
794
self,
795
x: torch.Tensor,
773
- mask: torch.Tensor,
796
freqs_cis: torch.Tensor,
797
+ cu_seqlens: torch.Tensor,
798
+ max_seqlen: torch.Tensor,
799
+ sequence_lengths: torch.Tensor | None,
800
) -> torch.Tensor:
801
batch, patches, _ = x.shape
802
@@ -782,15 +807,14 @@ class Attention(nn.Module):
807
v = v.reshape(batch, patches, self.n_heads, self.head_dim)
808
809
q, k = apply_rotary_emb_vit(q, k, freqs_cis=freqs_cis)
785
-
786
- if USE_XFORMERS_OPS:
787
- out = xops.memory_efficient_attention(q, k, v, attn_bias=mask)
788
- else:
789
- q = q.transpose(1, 2)
790
- k = k.transpose(1, 2)
791
- v = v.transpose(1, 2)
792
- out = nn.functional.scaled_dot_product_attention(q, k, v, attn_mask=mask)
793
- out = out.transpose(1, 2)
810
+ out = self.attn(
811
+ q,
812
+ k,
813
+ v,
814
+ cu_seqlens=cu_seqlens,
815
+ max_seqlen=max_seqlen,
816
+ sequence_lengths=sequence_lengths,
817
+ )
818
819
out = out.reshape(batch, patches, self.n_heads * self.head_dim)
820
out, _ = self.o_proj(out)
@@ -825,11 +849,17 @@ class TransformerBlock(nn.Module):
849
def forward(
850
self,
851
x: torch.Tensor,
828
- mask: torch.Tensor,
852
freqs_cis: torch.Tensor,
853
+ cu_seqlens: torch.Tensor,
854
+ max_seqlen: torch.Tensor,
855
+ sequence_lengths: torch.Tensor | None,
856
) -> torch.Tensor:
857
r = self.attention.forward(
832
- self.attention_norm(x), mask=mask, freqs_cis=freqs_cis
858
+ self.attention_norm(x),
859
+ freqs_cis=freqs_cis,
860
+ cu_seqlens=cu_seqlens,
861
+ max_seqlen=max_seqlen,
862
+ sequence_lengths=sequence_lengths,
863
)
864
h = x + r
865
r = self.feed_forward.forward(self.ffn_norm(h))
@@ -860,11 +890,19 @@ class Transformer(nn.Module):
890
def forward(
891
self,
892
x: torch.Tensor,
863
- mask: torch.Tensor,
893
freqs_cis: torch.Tensor | None,
894
+ cu_seqlens: torch.Tensor,
895
+ max_seqlen: torch.Tensor,
896
+ sequence_lengths: torch.Tensor | None,
897
) -> torch.Tensor:
898
for layer in self.layers:
867
- x = layer(x, mask=mask, freqs_cis=freqs_cis)
899
+ x = layer(
900
+ x,
901
+ freqs_cis=freqs_cis,
902
+ cu_seqlens=cu_seqlens,
903
+ max_seqlen=max_seqlen,
904
+ sequence_lengths=sequence_lengths,
905
+ )
906
return x
907
908
@@ -971,20 +1009,21 @@ class VisionTransformer(nn.Module):
1009
positions = position_meshgrid(patch_embeds_list).to(self.device)
1010
freqs_cis = self.freqs_cis[positions[:, 0], positions[:, 1]]
1011
974
- # pass through Transformer with a block diagonal mask delimiting images
975
- if USE_XFORMERS_OPS:
976
- mask = xops.fmha.attn_bias.BlockDiagonalMask.from_seqlens(
977
- [p.shape[-2] * p.shape[-1] for p in patch_embeds_list],
978
- )
979
- else:
980
- from transformers.models.pixtral.modeling_pixtral import (
981
- generate_block_attention_mask,
982
- )
983
-
984
- mask = generate_block_attention_mask(
985
- [p.shape[-2] * p.shape[-1] for p in patch_embeds_list], patch_embeds
986
- )
987
- out = self.transformer(patch_embeds, mask=mask, freqs_cis=freqs_cis)
1012
+ attention = self.transformer.layers[0].attention.attn
1013
+ cu_seqlens, max_seqlen, sequence_lengths = _make_packed_sequence_metadata(
1014
+ embed_sizes,
1015
+ attention.attn_backend,
1016
+ self.args.hidden_size,
1017
+ 1 if is_vit_use_data_parallel() else get_tensor_model_parallel_world_size(),
1018
+ patch_embeds.device,
1019
+ )
1020
+ out = self.transformer(
1021
+ patch_embeds,
1022
+ freqs_cis=freqs_cis,
1023
+ cu_seqlens=cu_seqlens,
1024
+ max_seqlen=max_seqlen,
1025
+ sequence_lengths=sequence_lengths,
1026
+ )
1027
1028
# squeeze dim 0 and split into separate tensors for each image
1029
return torch.split(out.squeeze(0), embed_sizes)
@@ -1244,36 +1283,39 @@ class PixtralHFAttention(nn.Module):
1283
1 if use_data_parallel else get_tensor_model_parallel_world_size()
1284
)
1285
self.n_heads = divide(config.num_attention_heads, self.tp_size)
1286
+ self.attn = MMEncoderAttention(
1287
+ num_heads=self.n_heads,
1288
+ head_size=self.head_dim,
1289
+ prefix=f"{prefix}.attn",
1290
+ )
1291
1292
def forward(
1293
self,
1294
hidden_states: torch.Tensor,
1251
- attention_mask: torch.Tensor,
1295
position_embeddings: torch.Tensor,
1296
+ cu_seqlens: torch.Tensor,
1297
+ max_seqlen: torch.Tensor,
1298
+ sequence_lengths: torch.Tensor | None,
1299
) -> tuple[torch.Tensor, torch.Tensor | None]:
1300
batch, patches, _ = hidden_states.size()
1301
1302
qkv_states, _ = self.qkv_proj(hidden_states)
1303
q, k, v = qkv_states.chunk(3, dim=-1)
1304
1259
- # Transpose q and k to apply HF's Rotary Position Embedding
1305
+ # Transpose q and k to apply HF's Rotary Position Embedding.
1306
q = q.view(batch, patches, self.n_heads, self.head_dim).transpose(1, 2)
1307
k = k.view(batch, patches, self.n_heads, self.head_dim).transpose(1, 2)
1308
v = v.view(batch, patches, self.n_heads, self.head_dim)
1309
cos, sin = position_embeddings
1310
q, k = apply_rotary_pos_emb(q, k, cos, sin, unsqueeze_dim=0)
1265
-
1266
- if USE_XFORMERS_OPS:
1267
- # Transpose q and k back for attention
1268
- q = q.transpose(1, 2).contiguous()
1269
- k = k.transpose(1, 2).contiguous()
1270
- out = xops.memory_efficient_attention(q, k, v, attn_bias=attention_mask)
1271
- else:
1272
- v = v.transpose(1, 2)
1273
- out = nn.functional.scaled_dot_product_attention(
1274
- q, k, v, attn_mask=attention_mask
1275
- )
1276
- out = out.transpose(1, 2)
1311
+ out = self.attn(
1312
+ q.transpose(1, 2).contiguous(),
1313
+ k.transpose(1, 2).contiguous(),
1314
+ v,
1315
+ cu_seqlens=cu_seqlens,
1316
+ max_seqlen=max_seqlen,
1317
+ sequence_lengths=sequence_lengths,
1318
+ )
1319
1320
out = out.reshape(batch, patches, self.n_heads * self.head_dim)
1321
attn_output, _ = self.o_proj(out)
@@ -1307,13 +1349,17 @@ class PixtralHFTransformerBlock(nn.Module):
1349
def forward(
1350
self,
1351
hidden_states: torch.Tensor,
1310
- attention_mask: torch.Tensor,
1352
position_embeddings: torch.Tensor,
1353
+ cu_seqlens: torch.Tensor,
1354
+ max_seqlen: torch.Tensor,
1355
+ sequence_lengths: torch.Tensor | None,
1356
) -> torch.Tensor:
1357
r, _ = self.attention.forward(
1358
self.attention_norm(hidden_states),
1315
- attention_mask=attention_mask,
1359
position_embeddings=position_embeddings,
1360
+ cu_seqlens=cu_seqlens,
1361
+ max_seqlen=max_seqlen,
1362
+ sequence_lengths=sequence_lengths,
1363
)
1364
h = hidden_states + r
1365
r = self.feed_forward.forward(self.ffn_norm(h))
@@ -1351,14 +1397,22 @@ class PixtralHFTransformer(nn.Module):
1397
def forward(
1398
self,
1399
x: torch.Tensor,
1354
- attention_mask: torch.Tensor,
1400
position_embeddings: torch.Tensor,
1401
+ cu_seqlens: torch.Tensor,
1402
+ max_seqlen: torch.Tensor,
1403
+ sequence_lengths: torch.Tensor | None,
1404
return_all_hidden_states: bool,
1405
) -> torch.Tensor:
1406
hidden_states_pool = [x]
1407
1408
for layer in self.layers:
1361
- x = layer(x, attention_mask, position_embeddings)
1409
+ x = layer(
1410
+ x,
1411
+ position_embeddings,
1412
+ cu_seqlens,
1413
+ max_seqlen,
1414
+ sequence_lengths,
1415
+ )
1416
if return_all_hidden_states:
1417
hidden_states_pool.append(x)
1418
# If we have multiple feature sample layers, we return all hidden
@@ -1453,23 +1507,21 @@ class PixtralHFVisionModel(nn.Module):
1507
).to(self.device)
1508
position_embedding = self.patch_positional_embedding(patch_embeds, position_ids)
1509
1456
- if USE_XFORMERS_OPS:
1457
- attention_mask = xops.fmha.attn_bias.BlockDiagonalMask.from_seqlens(
1458
- [p.shape[-2] * p.shape[-1] for p in patch_embeds_list],
1459
- )
1460
- else:
1461
- from transformers.models.pixtral.modeling_pixtral import (
1462
- generate_block_attention_mask,
1463
- )
1464
-
1465
- attention_mask = generate_block_attention_mask(
1466
- [p.shape[-2] * p.shape[-1] for p in patch_embeds_list], patch_embeds
1467
- )
1510
+ attention = self.transformer.layers[0].attention.attn
1511
+ cu_seqlens, max_seqlen, sequence_lengths = _make_packed_sequence_metadata(
1512
+ embed_sizes,
1513
+ attention.attn_backend,
1514
+ self.config.hidden_size,
1515
+ self.transformer.layers[0].attention.tp_size,
1516
+ patch_embeds.device,
1517
+ )
1518
1519
out = self.transformer(
1520
patch_embeds,
1471
- attention_mask,
1521
position_embedding,
1522
+ cu_seqlens,
1523
+ max_seqlen,
1524
+ sequence_lengths,
1525
return_all_hidden_states=select_layers is not None,
1526
)
1527