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, 04:30 UTC
[NIXL] heterogeneous block_size support (#26759)
Signed-off-by: Chendi Xue <chendi.xue@intel.com> Signed-off-by: Chendi.Xue <chendi.xue@intel.com> Co-authored-by: Nicolò Lucchesi <nicolo.lucchesi@gmail.com>
Chendi.Xue committed
Nov 14, 2025 at 23:51 UTC
c9e665852abbd42d7404a4f6dad7d47478ca95f8
3 files changed
+257
-59
tests/v1/kv_connector/nixl_integration/run_accuracy_test.sh
+4
@@ -49,6 +49,8 @@ NUM_DECODE_INSTANCES=${NUM_DECODE_INSTANCES:-1} # Default to 1
49
PREFILLER_TP_SIZE=${PREFILLER_TP_SIZE:-1}
50
DECODER_TP_SIZE=${DECODER_TP_SIZE:-1}
51
GPU_MEMORY_UTILIZATION=${GPU_MEMORY_UTILIZATION:-0.2}
52
+PREFILL_BLOCK_SIZE=${PREFILL_BLOCK_SIZE:-16}
53
+DECODE_BLOCK_SIZE=${DECODE_BLOCK_SIZE:-16}
54
55
# Find the git repository root directory
56
GIT_ROOT=$(git rev-parse --show-toplevel)
@@ -136,6 +138,7 @@ run_tests_for_model() {
138
vllm serve $model_name \
139
--port $PORT \
140
--enforce-eager \
141
+ --block-size ${PREFILL_BLOCK_SIZE} \
142
--gpu-memory-utilization $GPU_MEMORY_UTILIZATION \
143
--tensor-parallel-size $PREFILLER_TP_SIZE \
144
--kv-transfer-config '$KV_CONFIG'"
@@ -177,6 +180,7 @@ run_tests_for_model() {
180
vllm serve $model_name \
181
--port $PORT \
182
--enforce-eager \
183
+ --block-size ${DECODE_BLOCK_SIZE} \
184
--gpu-memory-utilization $GPU_MEMORY_UTILIZATION \
185
--kv-transfer-config '$KV_CONFIG'"
186
tests/v1/kv_connector/unit/test_nixl_connector.py
+3
@@ -407,6 +407,7 @@ class FakeNixlConnectorWorker(NixlConnectorWorker):
407
# `self.kv_cache_layout` is only forced to HND when vllm engine
408
# is started. We mock HND here.
409
kv_cache_layout="HND",
410
+ block_size=self.block_size,
411
),
412
remote_tp_size=remote_tp_size,
413
)
@@ -652,6 +653,7 @@ class TestNixlHandshake:
653
block_lens=worker.block_len_per_layer,
654
attn_backend_name=worker.backend_name,
655
kv_cache_layout=mismatched_layout,
656
+ block_size=worker.block_size,
657
)
658
659
with pytest.raises(RuntimeError):
@@ -706,6 +708,7 @@ class TestNixlHandshake:
708
block_lens=[i * 2 for i in worker.block_len_per_layer],
709
attn_backend_name=worker.backend_name,
710
kv_cache_layout="HND",
711
+ block_size=worker.block_size,
712
)
713
714
# We don't check layout for homogeneous TP and MLA for now, as the
vllm/distributed/kv_transfer/kv_connector/v1/nixl_connector.py
+250
-59
@@ -108,6 +108,7 @@ class NixlAgentMetadata(KVConnectorHandshakeMetadata):
108
block_lens: list[int]
109
attn_backend_name: str
110
kv_cache_layout: str
111
+ block_size: int
112
113
114
@dataclass
@@ -709,6 +710,9 @@ class NixlConnectorWorker:
710
self.is_mla or self._use_pallas or self.is_kv_layout_blocks_first
711
)
712
713
+ block_size: int
714
+ remote_block_size: dict[EngineId, int]
715
+
716
def tp_ratio(
717
self,
718
remote_tp_size: int,
@@ -725,6 +729,19 @@ class NixlConnectorWorker:
729
)
730
return self.tp_size // remote_tp_size
731
732
+ def block_size_ratio(
733
+ self,
734
+ remote_block_size: int,
735
+ ) -> float:
736
+ """
737
+ Calculate the block size ratio between local and remote TP.
738
+ """
739
+ assert self.block_size % remote_block_size == 0, (
740
+ f"Local block size {self.block_size} is not divisible "
741
+ f"by remote block size {remote_block_size} or vice versa."
742
+ )
743
+ return self.block_size // remote_block_size
744
+
745
def tp_ratio_from_engine_id(
746
self,
747
remote_engine_id: EngineId,
@@ -732,6 +749,13 @@ class NixlConnectorWorker:
749
remote_tp_size = self.remote_tp_size[remote_engine_id]
750
return self.tp_ratio(remote_tp_size)
751
752
+ def block_size_ratio_from_engine_id(
753
+ self,
754
+ remote_engine_id: EngineId,
755
+ ) -> float:
756
+ remote_block_size = self.remote_block_size[remote_engine_id]
757
+ return self.block_size_ratio(remote_block_size)
758
+
759
def is_kv_replicated(self, engine_id: EngineId) -> bool:
760
"""
761
Whether the KV cache is replicated across TP workers due to the
@@ -866,6 +890,7 @@ class NixlConnectorWorker:
890
891
# nixl_prepped_dlist_handle.
892
self.src_xfer_side_handle: int = 0
893
+ self.src_xfer_side_handles: dict[int, int] = {}
894
# Map of engine_id -> nixl_prepped_dlist_handle (int)].
895
self.dst_xfer_side_handles: dict[EngineId, int] = {}
896
@@ -925,6 +950,7 @@ class NixlConnectorWorker:
950
logger.debug("Detected kv cache layout %s", self.kv_cache_layout)
951
952
self._tp_size: dict[EngineId, int] = {self.engine_id: self.world_size}
953
+ self._block_size: dict[EngineId, int] = {self.engine_id: self.block_size}
954
# With heterogeneous TP, P must wait for all assigned D TP workers to
955
# finish reading before safely freeing the blocks.
956
self.consumer_notification_counts_by_req = defaultdict[ReqId, int](int)
@@ -936,6 +962,8 @@ class NixlConnectorWorker:
962
remote_tp_size=self._tp_size, # shared state
963
is_mla=self.use_mla,
964
total_num_kv_heads=self.model_config.get_total_num_kv_heads(),
965
+ block_size=self.block_size,
966
+ remote_block_size=self._block_size,
967
attn_backend=backend,
968
)
969
self._use_pallas = self.kv_topo._use_pallas
@@ -987,9 +1015,13 @@ class NixlConnectorWorker:
1015
)
1016
1017
# Register Remote agent.
1018
+ assert metadata.block_size <= self.block_size, (
1019
+ "nP > nD is not supported yet."
1020
+ )
1021
remote_agent_name = self.add_remote_agent(
1022
metadata, p_remote_rank, remote_tp_size
1023
)
1024
+
1025
setup_agent_time = time.perf_counter()
1026
logger.debug(
1027
"NIXL handshake: add agent took: %s",
@@ -1217,43 +1249,10 @@ class NixlConnectorWorker:
1249
self.num_regions *= 2
1250
1251
# Register local/src descr for NIXL xfer.
1220
- blocks_data = []
1221
- for i, base_addr in enumerate(seen_base_addresses):
1222
- kv_block_len = self.get_backend_aware_kv_block_len(layer_idx=i)
1223
- # NOTE With heter-TP, more blocks are prepared than what are
1224
- # needed as self.num_blocks >= nixl_agent_meta.num_blocks. We
1225
- # could create fewer, but then _get_block_descs_ids needs to
1226
- # select agent_meta.num_blocks instead of self.num_blocks for
1227
- # local descr, and that makes handling regular flow less clean.
1228
- for block_id in range(self.num_blocks):
1229
- block_offset = block_id * self.block_len_per_layer[i]
1230
- addr = base_addr + block_offset
1231
- # (addr, len, device id)
1232
- blocks_data.append((addr, kv_block_len, self.device_id))
1233
-
1234
- if self.kv_topo.is_kv_layout_blocks_first:
1235
- # Separate and interleave K/V regions to maintain the same
1236
- # descs ordering. This is needed for selecting contiguous heads
1237
- # when split across TP ranks.
1238
- for block_id in range(self.num_blocks):
1239
- block_offset = block_id * self.block_len_per_layer[i]
1240
- addr = base_addr + block_offset
1241
- # Register addresses for V cache (K registered first).
1242
- v_addr = addr + kv_block_len
1243
- blocks_data.append((v_addr, kv_block_len, self.device_id))
1244
- logger.debug(
1245
- "Created %s blocks for src engine %s and rank %s on device id %s",
1246
- len(blocks_data),
1247
- self.engine_id,
1248
- self.tp_rank,
1249
- self.device_id,
1250
- )
1252
+ self.seen_base_addresses = seen_base_addresses
1253
+ self.src_xfer_side_handle = self.register_local_xfer_handler(self.block_size)
1254
1252
- descs = self.nixl_wrapper.get_xfer_descs(blocks_data, self.nixl_memory_type)
1253
- # NIXL_INIT_AGENT to be used for preparations of local descs.
1254
- self.src_xfer_side_handle = self.nixl_wrapper.prep_xfer_dlist(
1255
- "NIXL_INIT_AGENT", descs
1256
- )
1255
+ self.src_xfer_side_handles[self.block_size] = self.src_xfer_side_handle
1256
1257
# TODO(mgoin): Hybrid memory allocator is currently disabled for
1258
# models with local attention (Llama 4). Can remove this once enabled.
@@ -1289,8 +1288,62 @@ class NixlConnectorWorker:
1288
kv_cache_layout=self.kv_cache_layout
1289
if not self.use_host_buffer
1290
else self.host_buffer_kv_cache_layout,
1291
+ block_size=self.block_size,
1292
+ )
1293
+
1294
+ def register_local_xfer_handler(
1295
+ self,
1296
+ block_size: int,
1297
+ ) -> int:
1298
+ """
1299
+ Function used for register local xfer handler with local block_size or
1300
+ Remote block_size.
1301
+
1302
+ When local block_size is same as remote block_size, we use local block_size
1303
+ to register local_xfer_handler during init.
1304
+
1305
+ When remote block size is less than local block size, we need to use
1306
+ register another local_xfer_handler using remote block len to ensure
1307
+ data copy correctness.
1308
+ """
1309
+ block_size_ratio = self.block_size // block_size
1310
+ blocks_data = []
1311
+ for i, base_addr in enumerate(self.seen_base_addresses):
1312
+ # The new block_len is using prefill block_len;
1313
+ # and num_blocks is multiple with N
1314
+ kv_block_len = (
1315
+ self.get_backend_aware_kv_block_len(layer_idx=i) // block_size_ratio
1316
+ )
1317
+ block_len_per_layer = self.block_len_per_layer[i] // block_size_ratio
1318
+ num_blocks = self.num_blocks * block_size_ratio
1319
+ for block_id in range(num_blocks):
1320
+ block_offset = block_id * block_len_per_layer
1321
+ addr = base_addr + block_offset
1322
+ # (addr, len, device id)
1323
+ blocks_data.append((addr, kv_block_len, self.device_id))
1324
+
1325
+ if self.kv_topo.is_kv_layout_blocks_first:
1326
+ # Separate and interleave K/V regions to maintain the same
1327
+ # descs ordering. This is needed for selecting contiguous heads
1328
+ # when split across TP ranks.
1329
+ for block_id in range(num_blocks):
1330
+ block_offset = block_id * block_len_per_layer
1331
+ addr = base_addr + block_offset
1332
+ # Register addresses for V cache (K registered first).
1333
+ v_addr = addr + kv_block_len
1334
+ blocks_data.append((v_addr, kv_block_len, self.device_id))
1335
+ logger.debug(
1336
+ "Created %s blocks for src engine %s and rank %s on device id %s",
1337
+ len(blocks_data),
1338
+ self.engine_id,
1339
+ self.tp_rank,
1340
+ self.device_id,
1341
)
1342
1343
+ descs = self.nixl_wrapper.get_xfer_descs(blocks_data, self.nixl_memory_type)
1344
+ # NIXL_INIT_AGENT to be used for preparations of local descs.
1345
+ return self.nixl_wrapper.prep_xfer_dlist("NIXL_INIT_AGENT", descs)
1346
+
1347
def add_remote_agent(
1348
self,
1349
nixl_agent_meta: NixlAgentMetadata,
@@ -1349,6 +1402,8 @@ class NixlConnectorWorker:
1402
### Register remote agent metadata
1403
if engine_id not in self._tp_size:
1404
self._tp_size[engine_id] = remote_tp_size
1405
+ if engine_id not in self._block_size:
1406
+ self._block_size[engine_id] = nixl_agent_meta.block_size
1407
1408
remote_agent_name = self.nixl_wrapper.add_remote_agent(
1409
nixl_agent_meta.agent_metadata
@@ -1359,6 +1414,13 @@ class NixlConnectorWorker:
1414
1415
# Create dst descs and xfer side handles. TP workers have same #blocks
1416
# so we only register once per engine_id.
1417
+ # Example:
1418
+ # block_size_ratio > 1:
1419
+ # remote: | 0| 1| 2| 3| 4| 5| 6| 7| 8| 9|10|11|12|
1420
+ # local origin:| 0| 1| 8| 12|
1421
+ # local mapped:| 0| 1| 2| 3| 4| 5| 6| 7| 8| 9|10|11|12|13|14|15|
1422
+ block_size_ratio = self.kv_topo.block_size_ratio_from_engine_id(engine_id)
1423
+
1424
if engine_id not in self.dst_num_blocks:
1425
self.dst_num_blocks[engine_id] = nixl_agent_meta.num_blocks
1426
@@ -1381,8 +1443,14 @@ class NixlConnectorWorker:
1443
# Register all remote blocks, but only the corresponding kv heads.
1444
for i, base_addr in enumerate(nixl_agent_meta.kv_caches_base_addr):
1445
kv_block_len = self.get_backend_aware_kv_block_len(layer_idx=i)
1446
+ remote_kv_block_len = kv_block_len // block_size_ratio
1447
+ if block_size_ratio > 1:
1448
+ # using remote kv_block_len as transfer unit
1449
+ kv_block_len = remote_kv_block_len
1450
rank_offset = (
1385
- self.tp_rank % tp_ratio * kv_block_len if not replicates_kv_cache else 0
1451
+ self.tp_rank % tp_ratio * remote_kv_block_len
1452
+ if not replicates_kv_cache
1453
+ else 0
1454
)
1455
for block_id in range(nixl_agent_meta.num_blocks):
1456
block_offset = block_id * nixl_agent_meta.block_lens[i]
@@ -1417,6 +1485,13 @@ class NixlConnectorWorker:
1485
remote_agent_name, descs
1486
)
1487
1488
+ if block_size_ratio > 1:
1489
+ # when prefill with smaller block_size, we need to init a
1490
+ # new handler with same block_len to match
1491
+ self.src_xfer_side_handles[nixl_agent_meta.block_size] = (
1492
+ self.register_local_xfer_handler(nixl_agent_meta.block_size)
1493
+ )
1494
+
1495
return remote_agent_name
1496
1497
def _validate_remote_agent_handshake(
@@ -1433,6 +1508,9 @@ class NixlConnectorWorker:
1508
assert nixl_agent_meta.attn_backend_name == self.backend_name
1509
1510
tp_ratio = self.kv_topo.tp_ratio_from_engine_id(remote_engine_id)
1511
+ block_size_ratio = self.kv_topo.block_size_ratio_from_engine_id(
1512
+ remote_engine_id
1513
+ )
1514
assert tp_ratio > 0, "Decode TP cannot be smaller than prefill TP"
1515
assert not self._use_pallas or tp_ratio == 1, (
1516
"TPU (pallas_v1) DOES NOT support heterogeneous TP yet."
@@ -1463,33 +1541,26 @@ class NixlConnectorWorker:
1541
remote_block_len = nixl_agent_meta.block_lens[0]
1542
if self.use_mla or self.kv_topo.is_kv_replicated(remote_engine_id):
1543
# With replicated KV cache, only the number of blocks can differ.
1466
- assert self.block_len_per_layer == nixl_agent_meta.block_lens, (
1467
- "KV cache sizes must match between P and D when replicated"
1468
- )
1469
- remote_block_size = remote_block_len // (self.slot_size_per_layer[0])
1544
+ for i in range(len(self.block_len_per_layer)):
1545
+ assert (
1546
+ self.block_len_per_layer[i] // block_size_ratio
1547
+ == nixl_agent_meta.block_lens[i]
1548
+ ), "KV cache sizes must match between P and D when replicated"
1549
else:
1550
# When MLA is not used, this is a list of the same block length
1551
for block_len in nixl_agent_meta.block_lens:
1552
assert block_len == remote_block_len, (
1553
"All remote layers must have the same block size"
1554
)
1476
- remote_block_size = remote_block_len // (
1477
- self.slot_size_per_layer[0] * tp_ratio
1478
- )
1479
- if self.kv_topo.is_kv_layout_blocks_first:
1480
- # With flashinfer, KV are sent in the same message.
1481
- remote_block_size //= 2
1555
1483
- assert remote_block_len == self.block_len_per_layer[0] * tp_ratio, (
1556
+ assert (
1557
+ remote_block_len
1558
+ == (self.block_len_per_layer[0] * tp_ratio) // block_size_ratio
1559
+ ), (
1560
"Remote P worker KV layer cache must be of shape [2, N, "
1561
"local_kv_heads*tp_ratio, block_size, head_dim] and same dtype."
1562
)
1563
1488
- assert self.block_size == remote_block_size, (
1489
- "Remote P worker with different page/block size is not supported "
1490
- f"{self.block_size=}, {remote_block_size=}"
1491
- )
1492
-
1564
# TP workers have same #blocks.
1565
assert self.dst_num_blocks[remote_engine_id] == nixl_agent_meta.num_blocks
1566
@@ -1576,6 +1647,56 @@ class NixlConnectorWorker:
1647
)
1648
cache.index_copy_(0, indices, permuted_blocks)
1649
1650
+ def blocksize_post_process(self, block_ids_per_ratio: dict[float, list[list[int]]]):
1651
+ def _process_local_gt_remote(blocks_to_update, block_size_ratio):
1652
+ n_kv_heads, block_size, head_size = blocks_to_update.shape[1:]
1653
+ remote_block_size = block_size // block_size_ratio
1654
+ n_blocks = block_size_ratio
1655
+ # actual permute is to convert
1656
+ # for local blocksize > remote blocksize
1657
+ # ex: local blocksize = 16 tokens, remote blocksize = 4 tokens
1658
+ # local block[0] = remote block[0, 1, 2, 3]
1659
+ # remote is |h0-b0|h1-b0|h2-b0|h3-b0|h0-b1|h1-b1|h2-b1|h3-b1|...
1660
+ # local is |h0-b0..................|h1-b0..................|...
1661
+ # permute is to:
1662
+ # 1. view => view remote as n_blocks * remote_shape(H,remoteN,D)
1663
+ # 2. permute => (H, nblocks, remoteN, D)
1664
+ # 3. flatten => (H, localN, D)
1665
+ permuted_blocks = (
1666
+ blocks_to_update.reshape(
1667
+ -1, n_blocks, n_kv_heads, remote_block_size, head_size
1668
+ )
1669
+ .permute(0, 2, 1, 3, 4)
1670
+ .flatten(2, 3)
1671
+ )
1672
+ return permuted_blocks
1673
+
1674
+ if len(self.device_kv_caches) == 0:
1675
+ return
1676
+ split_k_and_v = not (
1677
+ self.use_mla or self._use_pallas or self.kv_topo.is_kv_layout_blocks_first
1678
+ )
1679
+ sample_cache = list(self.device_kv_caches.values())[0][0]
1680
+ for block_size_ratio, block_ids_list in block_ids_per_ratio.items():
1681
+ assert block_size_ratio > 1, "Only nP < nD supported currently."
1682
+ block_ids_list = [[item for sublist in block_ids_list for item in sublist]]
1683
+
1684
+ for block_ids in block_ids_list:
1685
+ indices = torch.tensor(block_ids, device=sample_cache.device)
1686
+
1687
+ for _, cache_or_caches in self.device_kv_caches.items():
1688
+ cache_list = cache_or_caches if split_k_and_v else [cache_or_caches]
1689
+ for cache in cache_list:
1690
+ blocks_to_update = cache.index_select(0, indices)
1691
+ # because kv_cache is always using original layout NHD as
1692
+ # virtual shape while stride can be either HND / NHD at
1693
+ # initialization.
1694
+ # we need to firstly get physical view of the tensor
1695
+ permuted_blocks = _process_local_gt_remote(
1696
+ blocks_to_update.permute(0, 2, 1, 3), block_size_ratio
1697
+ ).permute(0, 2, 1, 3)
1698
+ cache.index_copy_(0, indices, permuted_blocks)
1699
+
1700
def get_finished(self) -> tuple[set[str], set[str]]:
1701
"""
1702
Get requests that are done sending or recving on this specific worker.
@@ -1599,6 +1720,7 @@ class NixlConnectorWorker:
1720
)
1721
1722
block_ids_to_permute = []
1723
+ block_ids_for_blocksize_post_process = defaultdict(list)
1724
for req_id in done_recving:
1725
# clean up metadata for completed requests
1726
meta = self._recving_metadata.pop(req_id, None)
@@ -1607,6 +1729,20 @@ class NixlConnectorWorker:
1729
self.sync_recved_kv_to_device(req_id, meta)
1730
if self.enable_permute_local_kv:
1731
block_ids_to_permute += meta.local_physical_block_ids
1732
+
1733
+ # post processing for heteroblocksize
1734
+ block_size_ratio = self.kv_topo.block_size_ratio_from_engine_id(
1735
+ meta.remote_engine_id
1736
+ )
1737
+ if (
1738
+ not self.use_mla
1739
+ and block_size_ratio > 1
1740
+ and self.kv_cache_layout == "HND"
1741
+ ):
1742
+ block_ids_for_blocksize_post_process[block_size_ratio].append(
1743
+ meta.local_block_ids
1744
+ )
1745
+ self.blocksize_post_process(block_ids_for_blocksize_post_process)
1746
if len(block_ids_to_permute) > 0:
1747
self.permute_device_kv(block_ids_to_permute)
1748
@@ -1781,6 +1917,24 @@ class NixlConnectorWorker:
1917
dst_engine_id: str,
1918
request_id: str,
1919
):
1920
+ block_size_ratio = self.kv_topo.block_size_ratio_from_engine_id(dst_engine_id)
1921
+ if block_size_ratio > 1:
1922
+ local_block_ids = self.get_mapped_blocks(
1923
+ np.asarray(local_block_ids), block_size_ratio
1924
+ )
1925
+ if len(local_block_ids) > len(remote_block_ids):
1926
+ # NOTE:
1927
+ # get_mapped_blocks will always expand block_ids for n times.
1928
+ # ex:
1929
+ # prefill block_ids with block_size as 4:
1930
+ # [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
1931
+ # Local decode block_ids with block_size as 16: [1, 2, 3]
1932
+ # expland ecode block_ids with get_mapped_blocks from [1, 2, 3] to
1933
+ # [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
1934
+ # Then we clip local to align with prefill
1935
+ # [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] to
1936
+ # [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
1937
+ local_block_ids = local_block_ids[: len(remote_block_ids)]
1938
# NOTE(rob): having the staging blocks be on the READER side is
1939
# not going to work well (since we will have to call rearrange tensors).
1940
# after we detect the txn is complete (which means we cannot make the
@@ -1823,7 +1977,10 @@ class NixlConnectorWorker:
1977
remote_block_ids = remote_block_ids[-num_local_blocks:]
1978
1979
# Get side handles.
1826
- local_xfer_side_handle = self.src_xfer_side_handle
1980
+ remote_block_size = self.kv_topo.remote_block_size[dst_engine_id]
1981
+ local_xfer_side_handle = self.src_xfer_side_handles.get(
1982
+ remote_block_size, self.src_xfer_side_handle
1983
+ )
1984
remote_xfer_side_handle = self.dst_xfer_side_handles[dst_engine_id]
1985
1986
# NOTE (nicolo) With homogeneous TP, each TP worker loads KV from
@@ -1833,13 +1990,17 @@ class NixlConnectorWorker:
1990
# Get descs ids.
1991
local_block_descs_ids: np.ndarray
1992
remote_block_descs_ids: np.ndarray
1993
+
1994
if not self.block_window_per_layer:
1995
# Default case: assume global attention
1996
remote_block_descs_ids = self._get_block_descs_ids(
1839
- dst_engine_id, remote_block_ids
1997
+ dst_engine_id,
1998
+ remote_block_ids,
1999
)
2000
local_block_descs_ids = self._get_block_descs_ids(
1842
- self.engine_id, local_block_ids
2001
+ self.engine_id,
2002
+ local_block_ids,
2003
+ block_size_ratio=block_size_ratio,
2004
)
2005
else:
2006
# TODO(mgoin): remove this once we have hybrid memory allocator
@@ -1860,10 +2021,15 @@ class NixlConnectorWorker:
2021
2022
# Get descs ids for the layer.
2023
layer_local_desc_ids = self._get_block_descs_ids(
1863
- self.engine_id, layer_local_block_ids, layer_idx
2024
+ dst_engine_id,
2025
+ layer_local_block_ids,
2026
+ layer_idx,
2027
)
2028
layer_remote_desc_ids = self._get_block_descs_ids(
1866
- dst_engine_id, layer_remote_block_ids, layer_idx
2029
+ self.engine_id,
2030
+ layer_remote_block_ids,
2031
+ layer_idx,
2032
+ block_size_ratio=block_size_ratio,
2033
)
2034
2035
local_descs_list.append(layer_local_desc_ids)
@@ -1905,8 +2071,31 @@ class NixlConnectorWorker:
2071
self.nixl_wrapper.release_xfer_handle(handle)
2072
self._failed_recv_reqs.add(request_id)
2073
2074
+ def get_mapped_blocks(self, block_ids, block_size_ratio):
2075
+ """
2076
+ Calculates the new set of block IDs by mapping every element
2077
+ in the (potentially sparse) input array.
2078
+ Example: block_ids=[0, 2], block_size_ratio=2
2079
+ get_mapped_blocks 0 1 [2 3] 4 5
2080
+ # remote is |h0-b0|h1-b0||h0-b1|h1-b1||h0-b1|h1-b1||
2081
+ # local is |h0-b0......||h1-b0......||h2-b0........
2082
+ local_block_ids 0 [1] 2
2083
+ """
2084
+ if block_ids.size == 0:
2085
+ return np.array([], dtype=np.int64)
2086
+
2087
+ start_ids = block_ids * block_size_ratio
2088
+ offsets = np.arange(block_size_ratio)
2089
+ mapped_2d = start_ids[:, None] + offsets[None, :]
2090
+
2091
+ return mapped_2d.flatten().astype(np.int64)
2092
+
2093
def _get_block_descs_ids(
1909
- self, engine_id: str, block_ids: list[int], layer_idx: int | None = None
2094
+ self,
2095
+ engine_id: str,
2096
+ block_ids: list[int],
2097
+ layer_idx: int | None = None,
2098
+ block_size_ratio: float | None = None,
2099
) -> np.ndarray:
2100
"""
2101
Get the descs ids for a set of block ids.
@@ -1929,6 +2118,8 @@ class NixlConnectorWorker:
2118
region_ids = np.arange(layer_idx, layer_idx + 1)
2119
2120
num_blocks = self.dst_num_blocks[engine_id]
2121
+ if block_size_ratio is not None:
2122
+ num_blocks = int(num_blocks * block_size_ratio)
2123
2124
# Compute the desc ids for each block.
2125
region_ids = region_ids[:, None]