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 11, 08:45 UTC
[Attention] Cache attention metadata builds across hybrid KV-cache groups (#29627)
Signed-off-by: Lucas Wilkinson <lwilkins@redhat.com> Co-authored-by: Stanislaw Wozniak <stw@zurich.ibm.com>
Lucas Wilkinson committed
Dec 16, 2025 at 17:10 UTC
9fec0e13d512b6b9082e40297582d8052f434610
7 files changed
+105
-13
tests/v1/attention/test_chunked_local_attention.py
+1
-1
@@ -172,7 +172,7 @@ def test_local_attention_virtual_batches(test_data: LocalAttentionTestData):
172
)
173
174
# Call the function
175
- result = make_local_attention_virtual_batches(
175
+ result, _ = make_local_attention_virtual_batches(
176
attn_chunk_size, common_attn_metadata, block_size
177
)
178
vllm/attention/layers/chunked_local_attention.py
+12
-4
@@ -4,7 +4,7 @@ import functools
4
5
import torch
6
7
-from vllm.attention.backends.abstract import AttentionBackend, AttentionMetadata
7
+from vllm.attention.backends.abstract import AttentionBackend
8
from vllm.attention.layer import Attention
9
from vllm.attention.selector import get_attn_backend
10
from vllm.config import CacheConfig
@@ -51,11 +51,19 @@ def create_chunked_local_attention_backend(
51
common_prefix_len: int,
52
common_attn_metadata: CommonAttentionMetadata,
53
fast_build: bool = False,
54
- ) -> AttentionMetadata:
55
- common_attn_metadata = make_local_attention_virtual_batches(
54
+ ):
55
+ cm, make_virtual_batches_block_table = make_local_attention_virtual_batches(
56
attention_chunk_size, common_attn_metadata, block_size
57
)
58
- return super().build(common_prefix_len, common_attn_metadata, fast_build)
58
+ metadata = super().build(common_prefix_len, cm, fast_build)
59
+ metadata.make_virtual_batches_block_table = make_virtual_batches_block_table
60
+ return metadata
61
+
62
+ def update_block_table(
63
+ self, metadata, blk_table: torch.Tensor, slot_mapping: torch.Tensor
64
+ ):
65
+ blk_table = metadata.make_virtual_batches_block_table(blk_table)
66
+ return super().update_block_table(metadata, blk_table, slot_mapping)
67
68
attn_backend = subclass_attention_backend(
69
name_prefix=prefix,
vllm/envs.py
+2
-2
@@ -207,7 +207,7 @@ if TYPE_CHECKING:
207
VLLM_USE_TRTLLM_RAGGED_DEEPSEEK_PREFILL: bool = False
208
VLLM_ENABLE_CUDAGRAPH_GC: bool = False
209
VLLM_LOOPBACK_IP: str = ""
210
- VLLM_ALLOW_CHUNKED_LOCAL_ATTN_WITH_HYBRID_KV_CACHE: bool = False
210
+ VLLM_ALLOW_CHUNKED_LOCAL_ATTN_WITH_HYBRID_KV_CACHE: bool = True
211
VLLM_ENABLE_RESPONSES_API_STORE: bool = False
212
VLLM_USE_TRTLLM_ATTENTION: str | None = None
213
VLLM_NVFP4_GEMM_BACKEND: str | None = None
@@ -1430,7 +1430,7 @@ environment_variables: dict[str, Callable[[], Any]] = {
1430
# kv-cache memory usage and enable longer contexts)
1431
# TODO(lucas): Remove this flag once latency regression is resolved.
1432
"VLLM_ALLOW_CHUNKED_LOCAL_ATTN_WITH_HYBRID_KV_CACHE": lambda: bool(
1433
- int(os.getenv("VLLM_ALLOW_CHUNKED_LOCAL_ATTN_WITH_HYBRID_KV_CACHE", "0"))
1433
+ int(os.getenv("VLLM_ALLOW_CHUNKED_LOCAL_ATTN_WITH_HYBRID_KV_CACHE", "1"))
1434
),
1435
# Enables support for the "store" option in the OpenAI Responses API.
1436
# When set to 1, vLLM's OpenAI server will retain the input and output
vllm/v1/attention/backends/flash_attn.py
+13
@@ -2,6 +2,7 @@
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
"""Attention layer with FlashAttention."""
4
5
+import copy
6
from dataclasses import dataclass
7
from typing import ClassVar
8
@@ -250,6 +251,7 @@ class FlashAttentionMetadataBuilder(AttentionMetadataBuilder[FlashAttentionMetad
251
if get_flash_attn_version() == 3
252
else AttentionCGSupport.UNIFORM_BATCH
253
)
254
+ supports_update_block_table: bool = True
255
256
def __init__(
257
self,
@@ -493,6 +495,17 @@ class FlashAttentionMetadataBuilder(AttentionMetadataBuilder[FlashAttentionMetad
495
)
496
return attn_metadata
497
498
+ def update_block_table(
499
+ self,
500
+ metadata: FlashAttentionMetadata,
501
+ blk_table: torch.Tensor,
502
+ slot_mapping: torch.Tensor,
503
+ ) -> FlashAttentionMetadata:
504
+ new_metadata = copy.copy(metadata)
505
+ new_metadata.block_table = blk_table
506
+ new_metadata.slot_mapping = slot_mapping
507
+ return new_metadata
508
+
509
def use_cascade_attention(self, *args, **kwargs) -> bool:
510
return use_cascade_attention(*args, **kwargs)
511
vllm/v1/attention/backends/mamba2_attn.py
+27
@@ -1,5 +1,6 @@
1
# SPDX-License-Identifier: Apache-2.0
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+import copy
4
import itertools
5
from dataclasses import dataclass
6
@@ -134,6 +135,8 @@ class Mamba2AttentionMetadata:
135
class Mamba2AttentionMetadataBuilder(
136
BaseMambaAttentionMetadataBuilder[Mamba2AttentionMetadata]
137
):
138
+ supports_update_block_table: bool = True
139
+
140
def __init__(
141
self,
142
kv_cache_spec: AttentionSpec,
@@ -346,3 +349,27 @@ class Mamba2AttentionMetadataBuilder(
349
num_computed_tokens_p=num_computed_tokens_p,
350
)
351
return attn_metadata
352
+
353
+ def update_block_table(
354
+ self,
355
+ metadata: Mamba2AttentionMetadata,
356
+ blk_table: torch.Tensor,
357
+ slot_mapping: torch.Tensor,
358
+ ) -> Mamba2AttentionMetadata:
359
+ new_metadata = copy.copy(metadata)
360
+ prefix_caching = self.vllm_config.cache_config.enable_prefix_caching
361
+ state_indices_t = blk_table if prefix_caching else blk_table[:, 0]
362
+ num_reqs = blk_table.shape[0]
363
+
364
+ # For CUDA graphs, copy to persistent buffer
365
+ if (
366
+ metadata.num_prefills == 0
367
+ and num_reqs <= self.decode_cudagraph_max_bs
368
+ and self.compilation_config.cudagraph_mode.has_full_cudagraphs()
369
+ ):
370
+ persistent_state_indices_t = self.state_indices_tensor[:num_reqs]
371
+ persistent_state_indices_t.copy_(state_indices_t, non_blocking=True)
372
+ state_indices_t = persistent_state_indices_t
373
+
374
+ new_metadata.state_indices_tensor = state_indices_t
375
+ return new_metadata
vllm/v1/attention/backends/utils.py
+27
-5
@@ -4,6 +4,7 @@ import abc
4
import enum
5
import functools
6
from abc import abstractmethod
7
+from collections.abc import Callable
8
from dataclasses import dataclass, field, fields, make_dataclass
9
from typing import (
10
TYPE_CHECKING,
@@ -317,6 +318,9 @@ class AttentionMetadataBuilder(abc.ABC, Generic[M]):
318
# If not, set this to None. Otherwise set it to the query
319
# length that will be pulled into the front of the batch.
320
reorder_batch_threshold: int | None = None
321
+ # Does this backend/builder support updating the block table in existing
322
+ # metadata
323
+ supports_update_block_table: bool = False
324
325
@abstractmethod
326
def __init__(
@@ -387,6 +391,21 @@ class AttentionMetadataBuilder(abc.ABC, Generic[M]):
391
"""
392
raise NotImplementedError
393
394
+ def update_block_table(
395
+ self,
396
+ metadata: M,
397
+ blk_table: torch.Tensor,
398
+ slot_mapping: torch.Tensor,
399
+ ) -> M:
400
+ """
401
+ Update the block table for the attention metadata.
402
+ Faster when theres multiple kv-cache groups that create virtually the
403
+ same metadata but just with different block tables.
404
+
405
+ Only needs to be implemented if supports_update_block_table is True.
406
+ """
407
+ raise NotImplementedError
408
+
409
def build_for_cudagraph_capture(
410
self, common_attn_metadata: CommonAttentionMetadata
411
) -> M:
@@ -603,7 +622,7 @@ def make_local_attention_virtual_batches(
622
attn_chunk_size: int,
623
common_attn_metadata: CommonAttentionMetadata,
624
block_size: int = 0,
606
-) -> CommonAttentionMetadata:
625
+) -> tuple[CommonAttentionMetadata, Callable[[torch.Tensor], torch.Tensor]]:
626
query_start_loc_np = common_attn_metadata.query_start_loc_cpu.numpy()
627
seq_lens_np = common_attn_metadata.seq_lens_cpu.numpy()
628
block_table = common_attn_metadata.block_table_tensor
@@ -715,9 +734,12 @@ def make_local_attention_virtual_batches(
734
# tensor first, which recovers perf.
735
batch_indices_torch = torch.from_numpy(batch_indices)
736
block_indices_torch = torch.from_numpy(block_indices)
718
- block_table_local = block_table[batch_indices_torch, block_indices_torch].view(
719
- virtual_batches, -1
720
- )
737
+
738
+ # Save as a lambda so we can return this for update_block_table
739
+ make_block_table = lambda block_table: block_table[
740
+ batch_indices_torch, block_indices_torch
741
+ ].view(virtual_batches, -1)
742
+ block_table_local = make_block_table(block_table)
743
744
query_start_loc_cpu = torch.from_numpy(cu_seqlens_q_local)
745
seq_lens_cpu = torch.from_numpy(seqlens_k_local)
@@ -736,7 +758,7 @@ def make_local_attention_virtual_batches(
758
causal=True,
759
_seq_lens_cpu=seq_lens_cpu,
760
_num_computed_tokens_cpu=torch.from_numpy(num_computed_tokens_local),
739
- )
761
+ ), make_block_table
762
763
764
def make_kv_sharing_fast_prefill_common_attn_metadata(
vllm/v1/worker/gpu_model_runner.py
+23
-1
@@ -1630,6 +1630,15 @@ class GPUModelRunner(
1630
logits_indices
1631
)
1632
1633
+ # Cache attention metadata builds across hybrid KV-cache groups
1634
+ # The only thing that changes between different hybrid KV-cache groups when the
1635
+ # same metadata builder and KVCacheSpec is the same is the block table, so we
1636
+ # can cache the attention metadata builds and just update the block table using
1637
+ # `builder.update_block_table` if the builder supports it.
1638
+ cached_attn_metadata: dict[
1639
+ tuple[KVCacheSpec, type[AttentionMetadataBuilder]], AttentionMetadata
1640
+ ] = {}
1641
+
1642
def _build_attn_group_metadata(
1643
kv_cache_gid: int,
1644
attn_gid: int,
@@ -1637,13 +1646,15 @@ class GPUModelRunner(
1646
ubid: int | None = None,
1647
) -> None:
1648
attn_group = self.attn_groups[kv_cache_gid][attn_gid]
1649
+ builder = attn_group.get_metadata_builder(ubid or 0)
1650
+ cache_key = (kv_cache_groups[kv_cache_gid].kv_cache_spec, type(builder))
1651
+
1652
cascade_attn_prefix_len = (
1653
cascade_attn_prefix_lens[kv_cache_gid][attn_gid]
1654
if cascade_attn_prefix_lens
1655
else 0
1656
)
1657
1646
- builder = attn_group.get_metadata_builder(ubid or 0)
1658
extra_attn_metadata_args = {}
1659
if use_spec_decode and isinstance(builder, GDNAttentionMetadataBuilder):
1660
assert ubid is None, "UBatching not supported with GDN yet"
@@ -1658,12 +1669,23 @@ class GPUModelRunner(
1669
attn_metadata_i = builder.build_for_cudagraph_capture(
1670
common_attn_metadata
1671
)
1672
+ elif (
1673
+ cache_key in cached_attn_metadata
1674
+ and builder.supports_update_block_table
1675
+ ):
1676
+ attn_metadata_i = builder.update_block_table(
1677
+ cached_attn_metadata[cache_key],
1678
+ common_attn_metadata.block_table_tensor,
1679
+ common_attn_metadata.slot_mapping,
1680
+ )
1681
else:
1682
attn_metadata_i = builder.build(
1683
common_prefix_len=cascade_attn_prefix_len,
1684
common_attn_metadata=common_attn_metadata,
1685
**extra_attn_metadata_args,
1686
)
1687
+ if builder.supports_update_block_table:
1688
+ cached_attn_metadata[cache_key] = attn_metadata_i
1689
1690
if ubid is None:
1691
assert isinstance(attn_metadata, dict)