@tduncan / Nvidia-Optimization-Stack-VLLM Mirror · read-only / commits / a7ef3eb0cd
Fork 0

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 13, 12:00 UTC

[NIXL] Generalize block-first backend layouts (FlashInfer-like) (#28282)

Nicolò Lucchesi committed Nov 11, 2025 at 17:57 UTC a7ef3eb0cd03e729c7a29914400e0ca928767999
2 files changed +52 -12
tests/v1/kv_connector/unit/test_nixl_connector.py
+15 -2
@@ -1096,7 +1096,8 @@ def _run_abort_timeout_test(llm: LLM, timeout: int):
1096 llm.llm_engine.engine_core.shutdown()
1097
1098
1099 -def test_register_kv_caches(dist_init):
1099 +@pytest.mark.parametrize("attn_backend", ["FLASH_ATTN", "TRITON_ATTN"])
1100 +def test_register_kv_caches(dist_init, attn_backend, monkeypatch):
1101 """
1102 Test that register_kv_caches() properly calls nixl_wrapper methods with
1103 correct data.
@@ -1108,10 +1109,22 @@ def test_register_kv_caches(dist_init):
1109 block layout info
1110 """
1111
1112 + monkeypatch.setenv("VLLM_ATTENTION_BACKEND", attn_backend)
1113 +
1114 vllm_config = create_vllm_config()
1115
1116 + # Import the appropriate backend based on the parameter
1117 + if attn_backend == "FLASH_ATTN":
1118 + from vllm.v1.attention.backends.flash_attn import FlashAttentionBackend
1119 +
1120 + backend_cls = FlashAttentionBackend
1121 + else: # TRITON_ATTN
1122 + from vllm.v1.attention.backends.triton_attn import TritonAttentionBackend
1123 +
1124 + backend_cls = TritonAttentionBackend
1125 +
1126 # Create test kv cache tensors using proper backend shape
1114 - kv_cache_shape = FlashAttentionBackend.get_kv_cache_shape(
1127 + kv_cache_shape = backend_cls.get_kv_cache_shape(
1128 num_blocks=2, block_size=16, num_kv_heads=4, head_size=64
1129 )
1130 shared_tensor = torch.zeros(*kv_cache_shape, dtype=torch.float16)
vllm/distributed/kv_transfer/kv_connector/v1/nixl_connector.py
+37 -10
@@ -21,6 +21,7 @@ import torch
21 import zmq
22
23 from vllm import envs
24 +from vllm.attention import AttentionBackend
25 from vllm.attention.backends.registry import AttentionBackendEnum
26 from vllm.attention.selector import get_attn_backend
27 from vllm.config import VllmConfig
@@ -669,6 +670,33 @@ class NixlConnectorWorker:
670 remote_tp_size: dict[EngineId, int]
671 is_mla: bool
672 total_num_kv_heads: int
673 + attn_backend: type[AttentionBackend]
674 +
675 + def __post_init__(self):
676 + # Figure out whether the first dimension of the cache is K/V
677 + # or num_blocks. This is used to register the memory regions correctly.
678 + kv_cache_shape = self.attn_backend.get_kv_cache_shape(
679 + num_blocks=1, block_size=16, num_kv_heads=1, head_size=1
680 + )
681 + # Non-MLA backends caches have 5 dims [2, num_blocks, H,N,D],
682 + # we just mock num_blocks to 1 for the dimension check below.
683 + self._is_kv_layout_blocks_first = (
684 + len(kv_cache_shape) == 5 and kv_cache_shape[0] == 1
685 + )
686 +
687 + attn_backend = AttentionBackendEnum[self.attn_backend.get_name()]
688 + self._use_pallas = attn_backend == AttentionBackendEnum.PALLAS
689 +
690 + @property
691 + def is_kv_layout_blocks_first(self) -> bool:
692 + return self._is_kv_layout_blocks_first
693 +
694 + @property
695 + def split_k_and_v(self) -> bool:
696 + # Whether to register regions for K and V separately (when present).
697 + return not (
698 + self.is_mla or self._use_pallas or self.is_kv_layout_blocks_first
699 + )
700
701 def tp_ratio(
702 self,
@@ -876,9 +904,6 @@ class NixlConnectorWorker:
904 use_mla=self.use_mla,
905 )
906 self.backend_name = backend.get_name()
879 - attn_backend = AttentionBackendEnum[self.backend_name]
880 - self._use_flashinfer = attn_backend == AttentionBackendEnum.FLASHINFER
881 - self._use_pallas = attn_backend == AttentionBackendEnum.PALLAS
907 self.kv_cache_layout = get_kv_cache_layout()
908 self.host_buffer_kv_cache_layout = self.kv_cache_layout
909 logger.debug("Detected attention backend %s", self.backend_name)
@@ -896,7 +921,9 @@ class NixlConnectorWorker:
921 remote_tp_size=self._tp_size, # shared state
922 is_mla=self.use_mla,
923 total_num_kv_heads=self.model_config.get_total_num_kv_heads(),
924 + attn_backend=backend,
925 )
926 + self._use_pallas = self.kv_topo._use_pallas
927
928 def _nixl_handshake(
929 self,
@@ -1076,7 +1103,7 @@ class NixlConnectorWorker:
1103 # (roughly 8KB vs 5KB).
1104 # Conversely for FlashInfer, K and V are registered in the same region
1105 # to better exploit the memory layout (ie num_blocks is the first dim).
1079 - split_k_and_v = not (self.use_mla or self._use_pallas or self._use_flashinfer)
1106 + split_k_and_v = self.kv_topo.split_k_and_v
1107 tensor_size_bytes = None
1108 # Enable different block lengths for different layers when MLA is used.
1109 self.block_len_per_layer = list[int]()
@@ -1141,7 +1168,7 @@ class NixlConnectorWorker:
1168
1169 self.device_kv_caches = kv_caches
1170 self.dst_num_blocks[self.engine_id] = self.num_blocks
1144 - if self._use_flashinfer:
1171 + if self.kv_topo.is_kv_layout_blocks_first:
1172 for i in range(len(self.slot_size_per_layer)):
1173 assert self.slot_size_per_layer[i] % 2 == 0
1174 self.slot_size_per_layer[i] //= 2
@@ -1169,7 +1196,7 @@ class NixlConnectorWorker:
1196 # (addr, len, device id)
1197 blocks_data.append((addr, kv_block_len, self.device_id))
1198
1172 - if self._use_flashinfer:
1199 + if self.kv_topo.is_kv_layout_blocks_first:
1200 # Separate and interleave K/V regions to maintain the same
1201 # descs ordering. This is needed for selecting contiguous heads
1202 # when split across TP ranks.
@@ -1331,7 +1358,7 @@ class NixlConnectorWorker:
1358 # (addr, len, device id)
1359 blocks_data.append((addr, kv_block_len, nixl_agent_meta.device_id))
1360
1334 - if self._use_flashinfer:
1361 + if self.kv_topo.is_kv_layout_blocks_first:
1362 # With FlashInfer index V separately to allow head splitting.
1363 for block_id in range(nixl_agent_meta.num_blocks):
1364 block_offset = block_id * nixl_agent_meta.block_lens[i]
@@ -1414,7 +1441,7 @@ class NixlConnectorWorker:
1441 remote_block_size = remote_block_len // (
1442 self.slot_size_per_layer[0] * tp_ratio
1443 )
1417 - if self._use_flashinfer:
1444 + if self.kv_topo.is_kv_layout_blocks_first:
1445 # With flashinfer, KV are sent in the same message.
1446 remote_block_size //= 2
1447
@@ -1494,7 +1521,7 @@ class NixlConnectorWorker:
1521 - cache.index_copy_(0, indices, permuted_blocks) # copy permuted kv back
1522
1523 """
1497 - split_k_and_v = not (self.use_mla or self._use_pallas or self._use_flashinfer)
1524 + split_k_and_v = self.kv_topo.split_k_and_v
1525 inv_order = [0, 2, 1, 3]
1526 sample_cache = list(self.device_kv_caches.values())[0][0]
1527 target_shape = list(sample_cache.shape)
@@ -1874,7 +1901,7 @@ class NixlConnectorWorker:
1901 For FlashInfer, this is half the length of the whole block, as K and V
1902 share the same region.
1903 """
1877 - if self._use_flashinfer:
1904 + if self.kv_topo.is_kv_layout_blocks_first:
1905 # For indexing only half (either just the K or V part).
1906 block_len = self.block_len_per_layer[layer_idx] // 2
1907 else: