@tduncan / Nvidia-Optimization-Stack-VLLM Mirror · read-only / commits / 098d844731
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 6, 21:45 UTC

[NIXL][1/N] Refactor `kernel_block_size` detection (#35752)

Signed-off-by: NickLucche <nlucches@redhat.com>

Nicolò Lucchesi committed Mar 11, 2026 at 09:11 UTC 098d844731c535c40c30498181de8f11f4b92cbb
5 files changed +126 -96
tests/v1/kv_connector/unit/test_nixl_connector.py
+57 -23
@@ -9,7 +9,7 @@ import textwrap
9 import time
10 import uuid
11 from collections import defaultdict
12 -from typing import Any
12 +from typing import Any, cast
13 from unittest.mock import MagicMock, patch
14
15 import msgspec
@@ -332,14 +332,22 @@ def test_kv_transfer_handshake(dist_init):
332
333 # Prefill connector will register KV cache to populate proper handshake
334 # metadata.
335 + # TODO this must match with values used in kv cache config
336 + kv_cache_config = make_kv_cache_config(block_size=16, num_blocks=2)
337 prefill_connector = NixlConnector(
336 - vllm_config, KVConnectorRole.WORKER, make_kv_cache_config(block_size=16)
338 + vllm_config, KVConnectorRole.WORKER, kv_cache_config
339 + )
340 + kv_cache_spec = cast(
341 + AttentionSpec, kv_cache_config.kv_cache_groups[0].kv_cache_spec
342 )
343 kv_cache_shape = FlashAttentionBackend.get_kv_cache_shape(
339 - num_blocks=2, block_size=16, num_kv_heads=4, head_size=64
344 + num_blocks=kv_cache_config.num_blocks,
345 + block_size=kv_cache_spec.block_size,
346 + num_kv_heads=kv_cache_spec.num_kv_heads,
347 + head_size=kv_cache_spec.head_size,
348 )
341 - shared_tensor = torch.zeros(*kv_cache_shape, dtype=torch.float16)
342 - unique_tensor = torch.zeros(*kv_cache_shape, dtype=torch.float16)
349 + shared_tensor = torch.zeros(*kv_cache_shape, dtype=kv_cache_spec.dtype)
350 + unique_tensor = torch.zeros(*kv_cache_shape, dtype=kv_cache_spec.dtype)
351 kv_caches = {
352 "layer0": shared_tensor,
353 "layer1": unique_tensor,
@@ -383,7 +391,7 @@ def test_kv_transfer_handshake(dist_init):
391
392 # Decode connector will be able to create handshake with the prefill connector.
393 decode_connector = NixlConnector(
386 - vllm_config, KVConnectorRole.WORKER, make_kv_cache_config(block_size=16)
394 + vllm_config, KVConnectorRole.WORKER, kv_cache_config
395 )
396 decode_connector.register_kv_caches(kv_caches)
397
@@ -525,11 +533,13 @@ class TestNixlHandshake:
533 request_id = "req_id"
534
535 # Test worker role in decode server.
528 - connector = NixlConnector(
529 - vllm_config, KVConnectorRole.WORKER, make_kv_cache_config(block_size=16)
530 - )
536 + kv_cache_config = make_kv_cache_config(block_size=16, num_blocks=2)
537 + connector = NixlConnector(vllm_config, KVConnectorRole.WORKER, kv_cache_config)
538 connector.connector_worker = FakeNixlConnectorWorker(
532 - vllm_config, connector.engine_id, hand_shake_latency=0
539 + vllm_config,
540 + connector.engine_id,
541 + hand_shake_latency=0,
542 + kv_cache_config=kv_cache_config,
543 )
544 assert isinstance(connector.connector_worker.nixl_wrapper, FakeNixlWrapper)
545 worker = connector.connector_worker
@@ -1479,18 +1489,22 @@ def test_register_kv_caches(
1489 patch(f"{nixl_module}.threading.Event"),
1490 patch(f"{nixl_module}.threading.Thread") as mock_thread,
1491 patch(f"{nixl_module}.get_current_attn_backend") as mock_get_attn_backend,
1492 + patch(f"{nixl_module}.get_current_attn_backends") as mock_get_attn_backends,
1493 ):
1494 # Ensure get_attn_backend returns the correct value due to
1495 # _cached_get_attn_backend returning the backend from previous
1496 # test run if not mocking.
1497 mock_get_attn_backend.return_value = backend_cls
1498 + mock_get_attn_backends.return_value = [backend_cls]
1499
1500 # Create connector
1489 - connector = NixlConnector(
1490 - vllm_config, KVConnectorRole.WORKER, make_kv_cache_config(block_size=16)
1491 - )
1501 + kv_cache_config = make_kv_cache_config(block_size=16, num_blocks=2)
1502 + connector = NixlConnector(vllm_config, KVConnectorRole.WORKER, kv_cache_config)
1503 connector.connector_worker = FakeNixlConnectorWorker(
1493 - vllm_config, connector.engine_id, hand_shake_latency=0
1504 + vllm_config,
1505 + connector.engine_id,
1506 + hand_shake_latency=0,
1507 + kv_cache_config=kv_cache_config,
1508 )
1509
1510 # Get the mock instance
@@ -1515,6 +1529,13 @@ def test_register_kv_caches(
1529 num_layers = 32
1530 block_size = 16
1531 num_blocks = 8
1532 + # Keep the fake worker's expected num_blocks in sync with the
1533 + # cross-layer tensor we are about to register.
1534 + worker_kv_cache_config = make_kv_cache_config(
1535 + block_size=block_size, num_blocks=num_blocks
1536 + )
1537 + connector.connector_worker.kv_cache_config = worker_kv_cache_config
1538 + connector.connector_worker.num_blocks = worker_kv_cache_config.num_blocks
1539 kv_cache_spec = AttentionSpec(
1540 block_size=block_size,
1541 num_kv_heads=4,
@@ -1568,11 +1589,17 @@ def test_register_kv_caches(
1589
1590 else:
1591 # Create test kv cache tensors using proper backend shape
1592 + kv_cache_spec = cast(
1593 + AttentionSpec, kv_cache_config.kv_cache_groups[0].kv_cache_spec
1594 + )
1595 kv_cache_shape = backend_cls.get_kv_cache_shape(
1572 - num_blocks=2, block_size=16, num_kv_heads=4, head_size=64
1596 + num_blocks=kv_cache_config.num_blocks,
1597 + block_size=kv_cache_spec.block_size,
1598 + num_kv_heads=kv_cache_spec.num_kv_heads,
1599 + head_size=kv_cache_spec.head_size,
1600 )
1574 - shared_tensor = torch.zeros(*kv_cache_shape, dtype=torch.float16)
1575 - unique_tensor = torch.zeros(*kv_cache_shape, dtype=torch.float16)
1601 + shared_tensor = torch.zeros(*kv_cache_shape, dtype=kv_cache_spec.dtype)
1602 + unique_tensor = torch.zeros(*kv_cache_shape, dtype=kv_cache_spec.dtype)
1603 kv_caches = {
1604 "layer0": shared_tensor,
1605 "layer1": unique_tensor,
@@ -1606,7 +1633,7 @@ def test_register_kv_caches(
1633 unique_tensor[1].data_ptr(),
1634 ]
1635 expected_num_entries = 4
1609 - expected_blocks_count = 8
1636 + expected_blocks_count = kv_cache_config.num_blocks * 4
1637
1638 # Execute register_kv_caches
1639 connector.register_kv_caches(kv_caches)
@@ -1639,7 +1666,7 @@ def test_register_kv_caches(
1666 num_blocks = 8
1667 expected_block_len = expected_tensor_size // num_blocks
1668 else:
1642 - num_blocks = 2
1669 + num_blocks = kv_cache_config.num_blocks
1670 if is_blocks_first:
1671 expected_block_len = expected_tensor_size // num_blocks // 2
1672 else:
@@ -2226,15 +2253,22 @@ def test_compatibility_hash_validation(
2253 "enforce_handshake_compat": enforce_handshake_compat
2254 },
2255 )
2256 + kv_cache_config = make_kv_cache_config(block_size=16, num_blocks=2)
2257 decode_connector = NixlConnector(
2230 - local_vllm_config, KVConnectorRole.WORKER, make_kv_cache_config(block_size=16)
2258 + local_vllm_config, KVConnectorRole.WORKER, kv_cache_config
2259 )
2260 decode_worker = decode_connector.connector_worker
2261 + kv_cache_spec = cast(
2262 + AttentionSpec, kv_cache_config.kv_cache_groups[0].kv_cache_spec
2263 + )
2264 kv_cache_shape = decode_worker.attn_backend.get_kv_cache_shape(
2234 - num_blocks=2, block_size=16, num_kv_heads=4, head_size=64
2265 + num_blocks=kv_cache_config.num_blocks,
2266 + block_size=kv_cache_spec.block_size,
2267 + num_kv_heads=kv_cache_spec.num_kv_heads,
2268 + head_size=kv_cache_spec.head_size,
2269 )
2236 - shared_tensor = torch.zeros(*kv_cache_shape, dtype=torch.float16)
2237 - unique_tensor = torch.zeros(*kv_cache_shape, dtype=torch.float16)
2270 + shared_tensor = torch.zeros(*kv_cache_shape, dtype=kv_cache_spec.dtype)
2271 + unique_tensor = torch.zeros(*kv_cache_shape, dtype=kv_cache_spec.dtype)
2272 kv_caches = {
2273 "layer0": shared_tensor,
2274 "layer1": unique_tensor,
tests/v1/worker/test_gpu_model_runner.py
+4 -16
@@ -38,7 +38,7 @@ from vllm.v1.kv_cache_interface import (
38 from vllm.v1.sample.metadata import SamplingMetadata
39 from vllm.v1.worker.gpu_input_batch import InputBatch
40 from vllm.v1.worker.gpu_model_runner import GPUModelRunner
41 -from vllm.v1.worker.utils import AttentionGroup, select_common_block_size
41 +from vllm.v1.worker.utils import select_common_block_size
42
43 BLOCK_SIZE = 16
44 NUM_BLOCKS = 10
@@ -203,37 +203,25 @@ def _make_kv_cache_spec() -> FullAttentionSpec:
203 def test_select_common_block_size_prefers_manager_block_size():
204 backend_a = _make_mock_backend_for_kernel_block_size([MultipleOf(32)])
205 backend_b = _make_mock_backend_for_kernel_block_size([64, MultipleOf(16)])
206 - attn_groups = [
207 - AttentionGroup(backend_a, [], [], _make_kv_cache_spec(), 0),
208 - AttentionGroup(backend_b, [], [], _make_kv_cache_spec(), 0),
209 - ]
206
211 - selected_size = select_common_block_size(128, attn_groups)
207 + selected_size = select_common_block_size(128, [backend_a, backend_b])
208 assert selected_size == 128
209
210
211 def test_select_common_block_size_uses_largest_shared_int():
212 backend_a = _make_mock_backend_for_kernel_block_size([128, 64])
213 backend_b = _make_mock_backend_for_kernel_block_size([64, 32])
218 - attn_groups = [
219 - AttentionGroup(backend_a, [], [], _make_kv_cache_spec(), 0),
220 - AttentionGroup(backend_b, [], [], _make_kv_cache_spec(), 0),
221 - ]
214
223 - selected_size = select_common_block_size(256, attn_groups)
215 + selected_size = select_common_block_size(256, [backend_a, backend_b])
216 assert selected_size == 64
217
218
219 def test_select_common_block_size_no_valid_option():
220 backend_a = _make_mock_backend_for_kernel_block_size([64])
221 backend_b = _make_mock_backend_for_kernel_block_size([MultipleOf(16)])
230 - attn_groups = [
231 - AttentionGroup(backend_a, [], [], _make_kv_cache_spec(), 0),
232 - AttentionGroup(backend_b, [], [], _make_kv_cache_spec(), 0),
233 - ]
222
223 with pytest.raises(ValueError):
236 - select_common_block_size(48, attn_groups)
224 + select_common_block_size(48, [backend_a, backend_b])
225
226
227 def test_update_states_new_request(model_runner, dist_init):
vllm/distributed/kv_transfer/kv_connector/utils.py
+35 -25
@@ -358,15 +358,6 @@ class TpKVTopology:
358 # stride_order to retrieve physical position of block_size
359 kv_cache_shape = tuple(kv_cache_shape[i] for i in kv_cache_stride_order)
360
361 - # In the default non-cross layers layout the block_size position
362 - # is logical while in the cross layers case it is the physical
363 - # position. This matches the shape of the actual kv cache tensors
364 - # passed at register_kv_caches()/register_cross_layers_kv_cache()
365 - block_size_position = kv_cache_shape.index(_MOCK_BLOCK_SIZE)
366 -
367 - assert block_size_position is not None
368 - self._block_size_position = -(len(kv_cache_shape) - block_size_position)
369 -
361 @property
362 def is_kv_layout_blocks_first(self) -> bool:
363 return self._is_kv_layout_blocks_first
@@ -390,10 +381,6 @@ class TpKVTopology:
381 def cross_layers_blocks(self) -> bool:
382 return self._cross_layers_blocks
383
393 - @property
394 - def block_size_position(self) -> int:
395 - return self._block_size_position
396 -
384 def tp_ratio(
385 self,
386 remote_tp_size: int,
@@ -484,23 +471,46 @@ class TpKVTopology:
471 return self.get_target_remote_ranks(remote_tp_size)
472
473
487 -def get_current_attn_backend(vllm_config: VllmConfig):
474 +def get_current_attn_backends(
475 + vllm_config: VllmConfig, layer_names: list[str] | None = None
476 +) -> list[type[AttentionBackend]]:
477 + """Get all distinct attention backends for the given layers.
478 +
479 + Args:
480 + vllm_config: The current vLLM configuration.
481 + layer_names: Optional list of layer names to scope the lookup.
482 + When None, all attention layers are considered.
483 +
484 + Returns:
485 + Deduplicated list of attention backend classes.
486 + """
487 layer_type = cast(type[Any], AttentionLayerBase)
489 - layers = get_layers_from_vllm_config(vllm_config, layer_type, None)
488 + layers = get_layers_from_vllm_config(vllm_config, layer_type, layer_names)
489 if layers:
491 - backend = next(iter(layers.values())).get_attn_backend()
492 - else:
493 - # Fallback for tests, when static_forward_context is empty.
494 - logger.debug(
495 - "No layers found in the vLLM config. "
496 - "Falling back to default attention backend."
497 - )
498 - from vllm.v1.attention.selector import get_attn_backend
490 + seen: dict[str, type[AttentionBackend]] = {}
491 + for layer in layers.values():
492 + backend = layer.get_attn_backend()
493 + seen[backend.full_cls_name()] = backend
494 + return list(seen.values())
495 +
496 + # Fallback for tests, when static_forward_context is empty.
497 + logger.debug(
498 + "No layers found in the vLLM config. Falling back to default attention backend."
499 + )
500 + from vllm.v1.attention.selector import get_attn_backend
501
500 - backend = get_attn_backend(
502 + return [
503 + get_attn_backend(
504 head_size=vllm_config.model_config.get_head_size(),
505 dtype=vllm_config.model_config.dtype,
506 kv_cache_dtype=vllm_config.cache_config.cache_dtype,
507 use_mla=vllm_config.model_config.use_mla,
508 )
506 - return backend
509 + ]
510 +
511 +
512 +def get_current_attn_backend(
513 + vllm_config: VllmConfig, layer_names: list[str] | None = None
514 +) -> type[AttentionBackend]:
515 + """Get the first attention backend for the given layers."""
516 + return get_current_attn_backends(vllm_config, layer_names)[0]
vllm/distributed/kv_transfer/kv_connector/v1/nixl_connector.py
+25 -27
@@ -13,7 +13,7 @@ from collections import defaultdict
13 from collections.abc import Iterator
14 from concurrent.futures import Future, ThreadPoolExecutor
15 from dataclasses import dataclass
16 -from typing import TYPE_CHECKING, Any
16 +from typing import TYPE_CHECKING, Any, cast
17
18 import msgspec
19 import numpy as np
@@ -27,6 +27,7 @@ from vllm.distributed.kv_transfer.kv_connector.utils import (
27 EngineId,
28 TpKVTopology,
29 get_current_attn_backend,
30 + get_current_attn_backends,
31 kv_postprocess_blksize_and_layout_on_receive,
32 kv_postprocess_blksize_on_receive,
33 kv_postprocess_layout_on_receive,
@@ -61,6 +62,7 @@ from vllm.v1.attention.backends.utils import get_kv_cache_layout
62 from vllm.v1.core.sched.output import SchedulerOutput
63 from vllm.v1.kv_cache_interface import FullAttentionSpec, MambaSpec, SlidingWindowSpec
64 from vllm.v1.worker.block_table import BlockTable
65 +from vllm.v1.worker.utils import select_common_block_size
66
67 if TYPE_CHECKING:
68 from vllm.v1.core.kv_cache_manager import KVCacheBlocks
@@ -945,7 +947,8 @@ class NixlConnectorWorker:
947
948 # Config.
949 self.vllm_config = vllm_config
948 - self.block_size = vllm_config.cache_config.block_size
950 + # mypy will complain on re-assignment otherwise.
951 + self.block_size: int = cast(int, vllm_config.cache_config.block_size)
952
953 if vllm_config.kv_transfer_config is None:
954 raise ValueError("kv_transfer_config must be set for NixlConnector")
@@ -993,7 +996,7 @@ class NixlConnectorWorker:
996 self.tp_rank = get_tensor_model_parallel_rank()
997 self.world_size = get_tensor_model_parallel_world_size()
998 self.tp_group = get_tp_group()
996 - self.num_blocks = 0
999 + self.num_blocks = kv_cache_config.num_blocks
1000 self.enable_permute_local_kv = False
1001
1002 # KV Caches and nixl tracking data.
@@ -1131,11 +1134,30 @@ class NixlConnectorWorker:
1134 self.xfer_stats = NixlKVConnectorStats()
1135
1136 self._physical_blocks_per_logical_kv_block = 1
1137 + self._sync_block_size_with_kernel()
1138
1139 self.enforce_compat_hash = self.kv_transfer_config.get_from_extra_config(
1140 "enforce_handshake_compat", True
1141 )
1142
1143 + def _sync_block_size_with_kernel(self) -> None:
1144 + backends = get_current_attn_backends(self.vllm_config)
1145 + kernel_block_size = select_common_block_size(self.block_size, backends)
1146 + if self.block_size != kernel_block_size:
1147 + logger.info_once(
1148 + "User-specified logical block size (%s) does not match"
1149 + " physical kernel block size (%s). Using the latter.",
1150 + self.block_size,
1151 + kernel_block_size,
1152 + )
1153 + assert self.block_size > kernel_block_size
1154 + self._physical_blocks_per_logical_kv_block = (
1155 + self.block_size // kernel_block_size
1156 + )
1157 + self.block_size = kernel_block_size
1158 + self._block_size[self.engine_id] = kernel_block_size
1159 + self.num_blocks *= self._physical_blocks_per_logical_kv_block
1160 +
1161 def _nixl_handshake(
1162 self,
1163 host: str,
@@ -1469,7 +1491,6 @@ class NixlConnectorWorker:
1491
1492 # Enable different block lengths for different layers when MLA is used.
1493 self.block_len_per_layer = list[int]()
1472 - self.slot_size_per_layer = list[int]() # HD bytes in kv terms
1494 for layer_name, cache_or_caches in xfer_buffers.items():
1495 cache_list = (
1496 cache_or_caches if self.kv_topo.split_k_and_v else [cache_or_caches]
@@ -1486,26 +1507,11 @@ class NixlConnectorWorker:
1507 logger.debug(
1508 "Registering layer %s with cache shape: %s", layer_name, cache.shape
1509 )
1489 - kernel_block_size = cache.shape[self.kv_topo.block_size_position]
1490 - if self.block_size != kernel_block_size:
1491 - logger.info_once(
1492 - "User-specified logical block size (%s) does not match"
1493 - " physical kernel block size (%s). Using the latter. ",
1494 - self.block_size,
1495 - kernel_block_size,
1496 - )
1497 - self._physical_blocks_per_logical_kv_block = (
1498 - self.block_size // kernel_block_size
1499 - )
1500 - self.block_size = kernel_block_size
1501 - self._block_size[self.engine_id] = kernel_block_size
1502 -
1510 seen_base_addresses.append(base_addr)
1511 curr_tensor_size_bytes = cache.numel() * cache.element_size()
1512
1513 if tensor_size_bytes is None:
1514 tensor_size_bytes = curr_tensor_size_bytes
1508 - self.num_blocks = cache.shape[0]
1515
1516 assert cache.shape[0] == self.num_blocks, (
1517 "All kv cache tensors must have the same number of blocks"
@@ -1514,9 +1520,6 @@ class NixlConnectorWorker:
1520 self.block_len_per_layer.append(
1521 curr_tensor_size_bytes // self.num_blocks
1522 )
1517 - self.slot_size_per_layer.append(
1518 - self.block_len_per_layer[-1] // self.block_size
1519 - )
1523
1524 if not self.use_mla:
1525 # Different kv cache shape is not supported by HeteroTP
@@ -1534,7 +1537,6 @@ class NixlConnectorWorker:
1537 "Different block lengths collected: %s", set(self.block_len_per_layer)
1538 )
1539 assert len(self.block_len_per_layer) == len(seen_base_addresses)
1537 - assert self.num_blocks != 0
1540
1541 self.kv_caches_base_addr[self.engine_id][self.tp_rank] = seen_base_addresses
1542 self.num_regions = len(caches_data)
@@ -1550,10 +1552,6 @@ class NixlConnectorWorker:
1552 self.dst_num_blocks[self.engine_id] = self.num_blocks
1553
1554 if self.kv_topo.is_kv_layout_blocks_first:
1553 - for i in range(len(self.slot_size_per_layer)):
1554 - assert self.slot_size_per_layer[i] % 2 == 0
1555 - self.slot_size_per_layer[i] //= 2
1556 -
1555 # NOTE (NickLucche) When FlashInfer is used, memory is registered
1556 # with joint KV for each block. This minimizes the overhead in
1557 # registerMem allowing faster descs queries. In order to be able to
vllm/v1/worker/utils.py
+5 -5
@@ -258,7 +258,8 @@ class AttentionGroup:
258
259
260 def select_common_block_size(
261 - kv_manager_block_size: int, attn_groups: list[AttentionGroup]
261 + kv_manager_block_size: int,
262 + backends: list[type[AttentionBackend]],
263 ) -> int:
264 """
265 Select a block size that is supported by all backends and is a factor of
@@ -269,7 +270,7 @@ def select_common_block_size(
270
271 Args:
272 kv_manager_block_size: Block size of KV cache.
272 - attn_groups: List of attention groups.
273 + backends: List of attention backend classes.
274
275 Returns:
276 The selected block size.
@@ -297,8 +298,6 @@ def select_common_block_size(
298 return False
299 return True
300
300 - backends = [group.backend for group in attn_groups]
301 -
301 # Case 1: if the block_size of kv cache manager is supported by all backends,
302 # return it directly.
303 if block_size_is_supported(backends, kv_manager_block_size):
@@ -356,8 +355,9 @@ def prepare_kernel_block_sizes(
355 if isinstance(kv_cache_spec, AttentionSpec):
356 # This is an attention backend that supports virtual block splitting.
357 kv_manager_block_size = kv_cache_group.kv_cache_spec.block_size
358 + group_backends = [g.backend for g in attn_groups[kv_cache_gid]]
359 selected_kernel_size = select_common_block_size(
360 - kv_manager_block_size, attn_groups[kv_cache_gid]
360 + kv_manager_block_size, group_backends
361 )
362 kernel_block_sizes.append(selected_kernel_size)
363 elif isinstance(kv_cache_spec, MambaSpec):