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 6, 02:00 UTC
[FlexAttention] allow custom mask mod (#37692)
Signed-off-by: Angel Li <liangel@meta.com>
liangel-02 committed
Mar 24, 2026 at 16:03 UTC
8c47fdfdb17c7bab93c70722b7adf71d87deddb9
2 files changed
+121
-16
tests/kernels/test_flex_attention.py
+51
@@ -14,6 +14,7 @@ from tests.v1.attention.utils import (
14
create_vllm_config,
15
)
16
from vllm.v1.attention.backends.flex_attention import (
17
+ BlockSparsityHint,
18
FlexAttentionMetadataBuilder,
19
physical_to_logical_mapping,
20
)
@@ -223,5 +224,55 @@ def test_physical_to_logical_mapping_handles_reused_blocks():
224
assert out2[0, 2].item() == 1
225
226
227
+@pytest.mark.skipif(
228
+ not torch.cuda.is_available() or TORCH_VERSION < DIRECT_BUILD_VERSION,
229
+ reason="CUDA not available or PyTorch version < 2.9",
230
+)
231
+def test_block_sparsity_hint_prunes_blocks():
232
+ """Test that BlockSparsityHint prunes KV blocks from the direct build path.
233
+
234
+ Uses a hint that only keeps the diagonal (q_block == kv_block) to verify
235
+ that off-diagonal blocks are excluded from the resulting BlockMask.
236
+ """
237
+ device = torch.device("cuda")
238
+
239
+ vllm_config = create_vllm_config(
240
+ model_name="facebook/opt-125m",
241
+ block_size=16,
242
+ max_model_len=1024,
243
+ )
244
+ kv_cache_spec = create_standard_kv_cache_spec(vllm_config)
245
+
246
+ batch_spec = BatchSpec(
247
+ seq_lens=[256],
248
+ query_lens=[256],
249
+ name="test_sparsity_hint",
250
+ )
251
+
252
+ common_attn_metadata = create_common_attn_metadata(
253
+ batch_spec, vllm_config.cache_config.block_size, device
254
+ )
255
+
256
+ builder = FlexAttentionMetadataBuilder(kv_cache_spec, [], vllm_config, device)
257
+
258
+ metadata_no_hint = builder.build(
259
+ common_prefix_len=0, common_attn_metadata=common_attn_metadata
260
+ )
261
+ metadata_no_hint.block_mask = metadata_no_hint._build_block_mask_direct()
262
+ assert metadata_no_hint.block_mask.kv_num_blocks.max().item() > 1
263
+
264
+ def diagonal_hint(q_block_idx, kv_block_idx, block_size):
265
+ return q_block_idx == kv_block_idx
266
+
267
+ metadata_with_hint = builder.build(
268
+ common_prefix_len=0, common_attn_metadata=common_attn_metadata
269
+ )
270
+ metadata_with_hint.block_sparsity_hint = BlockSparsityHint(
271
+ hint_fn=diagonal_hint,
272
+ )
273
+ metadata_with_hint.block_mask = metadata_with_hint._build_block_mask_direct()
274
+ assert metadata_with_hint.block_mask.kv_num_blocks.max().item() <= 1
275
+
276
+
277
if __name__ == "__main__":
278
pytest.main([__file__])
vllm/v1/attention/backends/flex_attention.py
+70
-16
@@ -3,9 +3,10 @@
3
"""Attention layer with FlexAttention."""
4
5
import math
6
+from collections.abc import Callable
7
from dataclasses import dataclass
8
from functools import cached_property
8
-from typing import ClassVar
9
+from typing import ClassVar, NamedTuple
10
11
import torch
12
import torch._dynamo.decorators
@@ -294,6 +295,27 @@ def causal_mask_mod(
295
return q_idx >= kv_idx
296
297
298
+# Type alias for the block sparsity hint callable signature.
299
+_block_sparsity_hint_signature = Callable[
300
+ [torch.Tensor, torch.Tensor, int], torch.Tensor
301
+]
302
+
303
+
304
+class BlockSparsityHint(NamedTuple):
305
+ """This prunes KV blocks from the BlockMask before the flex_attention kernel
306
+ is invoked, so that blocks that are fully masked never get loaded.
307
+ Use this with custom mask_mods that are sparse to avoid
308
+ the kernel iterating over all KV blocks unnecessarily.
309
+
310
+ Attributes:
311
+ hint_fn: (q_block_idx [num_tokens, 1], kv_block_idx [1, num_kv_blocks],
312
+ block_size int) -> bool Tensor [num_tokens, num_kv_blocks].
313
+ Returns True for block pairs that may contain non-masked elements.
314
+ """
315
+
316
+ hint_fn: _block_sparsity_hint_signature
317
+
318
+
319
@dataclass
320
class FlexAttentionMetadata:
321
causal: bool
@@ -335,6 +357,7 @@ class FlexAttentionMetadata:
357
transformed_score_mod: _score_mod_signature | None = None
358
sliding_window: int | None = None
359
mm_prefix_range: dict[int, list[tuple[int, int]]] | None = None
360
+ block_sparsity_hint: BlockSparsityHint | None = None
361
362
@cached_property
363
def logical_block_ids(self):
@@ -378,7 +401,7 @@ class FlexAttentionMetadata:
401
402
return is_valid, logical_q_idx, logical_kv_idx
403
381
- def get_causal_mask_mod(self) -> _mask_mod_signature:
404
+ def get_paged_mask_mod(self) -> _mask_mod_signature:
405
"""Creates the mask_mod function for FlexAttention.
406
407
This function creates the combined mask mod function that handles:
@@ -504,8 +527,9 @@ class FlexAttentionMetadata:
527
def get_mask_mod(self):
528
# Stage-1: initialize the base mask_mod
529
# (causal mask for decoder or bidirectional mask for encoder)
507
- if self.causal:
508
- mask_mod = self.get_causal_mask_mod()
530
+ has_custom_mask = self.logical_mask_mod is not causal_mask_mod
531
+ if self.causal or has_custom_mask:
532
+ mask_mod = self.get_paged_mask_mod()
533
else:
534
mask_mod = self.get_bidirectional_mask_mod()
535
# stage-2: add external mask_mod for special attention during
@@ -591,7 +615,9 @@ class FlexAttentionMetadata:
615
self.doc_ids, : cdiv(self.max_seq_len, self.block_size)
616
]
617
594
- if self.sliding_window and self.causal:
618
+ custom_hint = self.block_sparsity_hint is not None
619
+
620
+ if self.sliding_window or custom_hint:
621
device = used_pages.device
622
assert self.doc_ids is not None
623
token_indices = torch.arange(
@@ -602,10 +628,24 @@ class FlexAttentionMetadata:
628
- self.query_start_loc[self.doc_ids]
629
+ self.decode_offset[self.doc_ids]
630
)
605
- min_kv_idx = torch.clamp(logical_q_idx - (self.sliding_window - 1), min=0)
606
- min_block_idx = min_kv_idx // self.block_size
607
- sliding_mask = self.logical_block_ids >= min_block_idx[:, None]
608
- used_pages.masked_fill_(~sliding_mask, 0)
631
+
632
+ if self.sliding_window:
633
+ assert self.sliding_window is not None
634
+ min_kv_idx = torch.clamp(
635
+ logical_q_idx - (self.sliding_window - 1), min=0
636
+ )
637
+ min_block_idx = min_kv_idx // self.block_size
638
+ sliding_mask = self.logical_block_ids >= min_block_idx[:, None]
639
+ used_pages.masked_fill_(~sliding_mask, 0)
640
+ if custom_hint:
641
+ assert self.block_sparsity_hint is not None
642
+ q_block_idx = logical_q_idx // self.block_size
643
+ hint_mask = self.block_sparsity_hint.hint_fn(
644
+ q_block_idx[:, None],
645
+ self.logical_block_ids[None, :],
646
+ self.block_size,
647
+ )
648
+ used_pages.masked_fill_(~hint_mask, 0)
649
650
used_pages_padded = pad_to_multiple(
651
used_pages, multiple=self.q_block_size, dim=0
@@ -660,11 +700,6 @@ class FlexAttentionMetadata:
700
self.mask_mod = self.get_mask_mod()
701
self.transformed_score_mod = self.get_transformed_score_mod()
702
663
- if self.direct_build and self.causal:
664
- self.block_mask = self._build_block_mask_direct()
665
- else:
666
- self.block_mask = self.build_block_mask()
667
-
703
704
class FlexAttentionMetadataBuilder(AttentionMetadataBuilder[FlexAttentionMetadata]):
705
def __init__(
@@ -770,6 +805,8 @@ class FlexAttentionImpl(AttentionImpl):
805
alibi_slopes: torch.Tensor | None
806
logits_soft_cap: float | None
807
mm_prefix_range: dict[int, list[tuple[int, int]]] | None = None
808
+ logical_mask_mod: _mask_mod_signature | None = None
809
+ block_sparsity_hint: BlockSparsityHint | None = None
810
811
def __init__(
812
self,
@@ -907,8 +944,25 @@ class FlexAttentionImpl(AttentionImpl):
944
attn_metadata.mask_mod = attn_metadata.get_mask_mod()
945
needs_rebuild_block_mask = True
946
910
- if needs_rebuild_block_mask:
911
- if attn_metadata.direct_build and attn_metadata.causal:
947
+ layer_mask_mod = getattr(layer, "logical_mask_mod", None)
948
+ if (
949
+ layer_mask_mod is not None
950
+ and attn_metadata.logical_mask_mod is not layer_mask_mod
951
+ ):
952
+ attn_metadata.logical_mask_mod = layer_mask_mod
953
+ attn_metadata.mask_mod = attn_metadata.get_mask_mod()
954
+ needs_rebuild_block_mask = True
955
+
956
+ layer_hint = getattr(layer, "block_sparsity_hint", None)
957
+ if (
958
+ layer_hint is not None
959
+ and attn_metadata.block_sparsity_hint is not layer_hint
960
+ ):
961
+ attn_metadata.block_sparsity_hint = layer_hint
962
+ needs_rebuild_block_mask = True
963
+
964
+ if needs_rebuild_block_mask or attn_metadata.block_mask is None:
965
+ if attn_metadata.direct_build:
966
attn_metadata.block_mask = attn_metadata._build_block_mask_direct()
967
else:
968
attn_metadata.block_mask = attn_metadata.build_block_mask()