Vectorize prep xfer list creation (#48209)

Signed-off-by: Ilia Yastrebov <iyastrebov@nvidia.com>

Ilia Yastrebov committed Jul 16, 2026 at 11:39 UTC 9f8cbfd8eb01de1310caa0b657840cbaf6c3b015
3 files changed +85 -74
tests/v1/kv_connector/unit/test_nixl_connector.py
+14 -7
@@ -13,6 +13,7 @@ from typing import Any, cast
13 from unittest.mock import MagicMock, patch
14
15 import msgspec
16 +import numpy as np
17 import pytest
18 import ray
19 import torch
@@ -750,7 +751,10 @@ class TestNixlHandshake:
751 worker.block_len_per_layer = [4096 * worker.block_size]
752 worker.num_blocks = 1
753 worker.dst_num_blocks[worker.engine_id] = worker.num_blocks
753 - worker.src_blocks_data = [(0, worker.block_len_per_layer[0], worker.tp_rank)]
754 + worker.src_blocks_data = np.array(
755 + [(0, worker.block_len_per_layer[0], worker.tp_rank)],
756 + dtype=np.uint64,
757 + )
758 worker.num_descs = len(worker.src_blocks_data)
759
760 def check_handshake(remote_tp_size: int):
@@ -1124,8 +1128,8 @@ class TestNixlHandshake:
1128 == worker._mamba_ssm_size[1]
1129 )
1130
1127 - assert worker._build_fa_remote(plan, meta, block_size_ratio=1) == [
1128 - (0x1000 + local_block_len, local_block_len, 0)
1131 + assert worker._build_fa_remote(plan, meta, block_size_ratio=1).tolist() == [
1132 + [0x1000 + local_block_len, local_block_len, 0]
1133 ]
1134
1135 @patch(
@@ -1158,10 +1162,13 @@ class TestNixlHandshake:
1162 worker._region_is_mla = [False, True]
1163 worker.num_blocks = 1
1164 worker.dst_num_blocks[worker.engine_id] = worker.num_blocks
1161 - worker.src_blocks_data = [
1162 - (0, fa_len, worker.tp_rank),
1163 - (0, idx_len, worker.tp_rank),
1164 - ]
1165 + worker.src_blocks_data = np.array(
1166 + [
1167 + (0, fa_len, worker.tp_rank),
1168 + (0, idx_len, worker.tp_rank),
1169 + ],
1170 + dtype=np.uint64,
1171 + )
1172 worker.num_descs = len(worker.src_blocks_data)
1173
1174 # D_TP=2, P_TP=1 -> tp_ratio=2. SPLIT region scales by tp_ratio;
tests/v1/kv_connector/unit/test_tp_mapping.py
+13 -6
@@ -11,6 +11,7 @@ from __future__ import annotations
11
12 from types import SimpleNamespace
13
14 +import numpy as np
15 import pytest
16
17 from vllm.distributed.kv_transfer.kv_connector.v1.nixl.tp_mapping import (
@@ -102,7 +103,10 @@ class TestBuildSrcSplitHandles:
103 )
104
105 worker = _make_mock_worker_for_splits((FullAttentionSpec,))
105 - src_blocks_data = [(0x2000 + i * 1024, 1024, 0) for i in range(8)]
106 + src_blocks_data = np.array(
107 + [(0x2000 + i * 1024, 1024, 0) for i in range(8)],
108 + dtype=np.uint64,
109 + )
110 num_descs = len(src_blocks_data)
111 splits = list(
112 worker._build_local_splits_from_plan(
@@ -135,11 +139,14 @@ class TestMambaPlanSplitHandles:
139
140 worker = _make_mock_worker_for_splits((FullAttentionSpec, MambaSpec))
141 # 2 FA descs + 1 SSM desc
138 - src_blocks_data = [
139 - (1000, 200, 0), # FA desc 0
140 - (2000, 200, 0), # FA desc 1
141 - (3000, 400, 0), # SSM desc 0
142 - ]
142 + src_blocks_data = np.array(
143 + [
144 + (1000, 200, 0), # FA desc 0
145 + (2000, 200, 0), # FA desc 1
146 + (3000, 400, 0), # SSM desc 0
147 + ],
148 + dtype=np.uint64,
149 + )
150
151 splits = list(worker._build_local_splits_from_plan(plan, src_blocks_data, 2))
152
vllm/distributed/kv_transfer/kv_connector/v1/nixl/base_worker.py
+58 -61
@@ -160,7 +160,7 @@ class NixlBaseConnectorWorker:
160 def _build_local_splits_from_plan(
161 self,
162 plan: TPMapping,
163 - src_blocks_data: list[tuple[int, int, int]],
163 + src_blocks_data: np.ndarray,
164 num_fa_descs: int,
165 ) -> Iterator[list[tuple[int, int, int]]]:
166 """Build split handle data for P_TP > D_TP scenario.
@@ -188,12 +188,13 @@ class NixlBaseConnectorWorker:
188
189 # Per-FA-descriptor replicate flag, in _build_fa_local emission order.
190 fa_desc_replicated = self._fa_desc_replicated(num_fa_descs)
191 + src_blocks_list = src_blocks_data.tolist()
192
193 for p_idx, p_rank in enumerate(plan.all_source_ranks):
194 fa_slot = plan.rank_to_attention_slot.get(p_rank, 0)
195
196 handle: list[tuple[int, int, int]] = []
196 - for j, (addr, local_len, dev) in enumerate(src_blocks_data):
197 + for j, (addr, local_len, dev) in enumerate(src_blocks_list):
198 if j < num_fa_descs:
199 if fa_desc_replicated[j]:
200 # REPLICATE (MLA): whole block written on every rank.
@@ -1277,51 +1278,48 @@ class NixlBaseConnectorWorker:
1278 self,
1279 base_addresses: list[int],
1280 block_size_ratio: int,
1280 - ) -> list[tuple[int, int, int]]:
1281 + ) -> np.ndarray:
1282 """Build desc regions (conv sub-projections + ssm) per layer for
1282 - local mamba blocks with DS conv layout."""
1283 + local mamba blocks with DS conv layout, as an Nx3 uint64 array."""
1284 assert block_size_ratio == 1, (
1285 "Mamba 3-read transfer with block_size_ratio != 1 is not tested. "
1286 f"Got block_size_ratio={block_size_ratio}."
1287 )
1288 + assert base_addresses, "Local KV cache base addresses must not be empty."
1289 assert self._conv_decomp is not None
1290 conv_offsets = self._conv_decomp.local_conv_offsets
1291 conv_size, ssm_size = self._mamba_ssm_size
1292 num_blocks = self._logical_num_blocks * block_size_ratio
1293 physical_per_logical = self._physical_blocks_per_logical_kv_block
1294 + device_id = self.device_id
1295 + block_arange = np.arange(num_blocks, dtype=np.uint64)
1296
1293 - result: list[tuple[int, int, int]] = []
1297 + parts: list[np.ndarray] = []
1298 for i, base_addr in enumerate(base_addresses):
1299 # Jump one page_size, but ssm page_size may be bigger when kernel
1300 # locks block size to a specific value (physical_per_logical scale).
1301 page_stride = (
1302 self.block_len_per_layer[i] // block_size_ratio * physical_per_logical
1303 )
1304 + blk_addrs = base_addr + block_arange * page_stride
1305 for off, sz in conv_offsets:
1301 - for blk in range(num_blocks):
1302 - result.append(
1303 - (base_addr + blk * page_stride + off, sz, self.device_id)
1304 - )
1306 + parts.append(self._stack_descs(blk_addrs + off, sz, device_id))
1307 # SSM temporal state follows the conv state.
1306 - for blk in range(num_blocks):
1307 - result.append(
1308 - (
1309 - base_addr + blk * page_stride + conv_size,
1310 - ssm_size,
1311 - self.device_id,
1312 - )
1313 - )
1314 - return result
1308 + parts.append(self._stack_descs(blk_addrs + conv_size, ssm_size, device_id))
1309 + return np.concatenate(parts)
1310
1311 def _build_mamba_remote(
1312 self,
1313 nixl_agent_meta: NixlAgentMetadata,
1314 tp_ratio: int,
1315 transfer_info: EngineTransferInfo,
1321 - ) -> list[tuple[int, int, int]]:
1316 + ) -> np.ndarray:
1317 """Build remote desc regions (conv sub-projections + ssm) per layer.
1318 For hetero-TP, each D rank reads only its sub-projection slice from
1324 - the P rank."""
1319 + the P rank. Returns an Nx3 uint64 array."""
1320 + assert nixl_agent_meta.kv_caches_base_addr, (
1321 + "Remote KV cache base addresses must not be empty."
1322 + )
1323 assert self._conv_decomp is not None
1324 effective_ratio = max(tp_ratio, 1)
1325 # Mamba conv state is always TP-sharded, even when attention KV
@@ -1338,35 +1336,41 @@ class NixlBaseConnectorWorker:
1336 remote_physical_per_logical = transfer_info.remote_physical_blocks_per_logical
1337 num_blocks = nixl_agent_meta.num_blocks // remote_physical_per_logical
1338 device_id = nixl_agent_meta.device_id
1339 + block_arange = np.arange(num_blocks, dtype=np.uint64)
1340
1342 - result: list[tuple[int, int, int]] = []
1341 + parts: list[np.ndarray] = []
1342 # NOTE (ZhanqiuHu): use per-layer block_lens[i], not [0], in case
1343 # block lengths vary across layers (e.g. MLA).
1344 for i, base_addr in enumerate(nixl_agent_meta.kv_caches_base_addr):
1345 page_stride = nixl_agent_meta.block_lens[i] * remote_physical_per_logical
1346 + blk_addrs = base_addr + block_arange * page_stride
1347 for off, sz in conv_offsets:
1348 - for blk in range(num_blocks):
1349 - result.append((base_addr + blk * page_stride + off, sz, device_id))
1348 + parts.append(self._stack_descs(blk_addrs + off, sz, device_id))
1349 # SSM temporal state is also TP-sharded on the heads dimension.
1351 - for blk in range(num_blocks):
1352 - ssm_addr = (
1353 - base_addr
1354 - + blk * page_stride
1355 - + conv_size_remote
1356 - + local_offset * ssm_read_size
1357 - )
1358 - result.append((ssm_addr, ssm_read_size, device_id))
1359 - return result
1350 + ssm_addrs = blk_addrs + conv_size_remote + local_offset * ssm_read_size
1351 + parts.append(self._stack_descs(ssm_addrs, ssm_read_size, device_id))
1352 + return np.concatenate(parts)
1353 +
1354 + @staticmethod
1355 + def _stack_descs(addrs: np.ndarray, length: int, device_id: int) -> np.ndarray:
1356 + out = np.empty((addrs.shape[0], 3), dtype=np.uint64)
1357 + out[:, 0] = addrs
1358 + out[:, 1] = length
1359 + out[:, 2] = device_id
1360 + return out
1361
1362 def _build_fa_local(
1363 self,
1364 base_addresses: list[int],
1365 block_size_ratio: int,
1365 - ) -> list[tuple[int, int, int]]:
1366 - """Build local FA descriptors for all layers."""
1366 + ) -> np.ndarray:
1367 + """Build local FA descriptors for all layers as an Nx3 uint64 array."""
1368 assert self.transfer_topo is not None
1369 + assert base_addresses, "Local KV cache base addresses must not be empty."
1370 num_blocks = self.num_blocks * block_size_ratio
1369 - result: list[tuple[int, int, int]] = []
1371 + device_id = self.device_id
1372 + block_arange = np.arange(num_blocks, dtype=np.uint64)
1373 + parts: list[np.ndarray] = []
1374 for i, base_addr in enumerate(base_addresses):
1375 kv_block_len = (
1376 self.get_backend_aware_kv_block_len(
@@ -1375,20 +1379,21 @@ class NixlBaseConnectorWorker:
1379 // block_size_ratio
1380 )
1381 page_stride = self.block_len_per_layer[i] // block_size_ratio
1378 - for block_id in range(num_blocks):
1379 - block_offset = block_id * page_stride
1380 - addr = base_addr + block_offset
1381 - result.append((addr, kv_block_len, self.device_id))
1382 - return result
1382 + addrs = base_addr + block_arange * page_stride
1383 + parts.append(self._stack_descs(addrs, kv_block_len, device_id))
1384 + return np.concatenate(parts)
1385
1386 def _build_fa_remote(
1387 self,
1388 plan: TPMapping,
1389 nixl_agent_meta: NixlAgentMetadata,
1390 block_size_ratio: int,
1389 - ) -> list[tuple[int, int, int]]:
1390 - """Build remote FA descriptors for all layers."""
1391 + ) -> np.ndarray:
1392 + """Build remote FA descriptors for all layers as an Nx3 uint64 array."""
1393 assert self.transfer_topo is not None
1394 + assert nixl_agent_meta.kv_caches_base_addr, (
1395 + "Remote KV cache base addresses must not be empty."
1396 + )
1397 fa_group_idx = next(
1398 i for i, t in enumerate(self._group_spec_types) if _is_attention_spec(t)
1399 )
@@ -1396,7 +1401,9 @@ class NixlBaseConnectorWorker:
1401 # per-rank offset; REPLICATE regions read the whole block once.
1402 split_reads = len(plan.source_ranks_per_group[fa_group_idx])
1403 num_blocks = nixl_agent_meta.num_blocks
1399 - result: list[tuple[int, int, int]] = []
1404 + device_id = nixl_agent_meta.device_id
1405 + block_arange = np.arange(num_blocks, dtype=np.uint64)
1406 + parts: list[np.ndarray] = []
1407 for i, base_addr in enumerate(nixl_agent_meta.kv_caches_base_addr):
1408 replicated = self._is_region_replicated(i)
1409 # Read our whole local region size from remote..
@@ -1417,18 +1424,14 @@ class NixlBaseConnectorWorker:
1424 local_block_len = local_block_len // num_reads
1425
1426 page_size = nixl_agent_meta.block_lens[i]
1420 - for block_id in range(num_blocks):
1421 - block_offset = block_id * page_size
1422 - # For each block, grab the kv heads chunk belonging to current local
1423 - # tp rank of size local_block_len.
1424 - addr = base_addr + block_offset + rank_offset
1425 - result.append((addr, local_block_len, nixl_agent_meta.device_id))
1426 - return result
1427 + addrs = base_addr + rank_offset + block_arange * page_size
1428 + parts.append(self._stack_descs(addrs, local_block_len, device_id))
1429 + return np.concatenate(parts)
1430
1431 def register_local_xfer_handler(
1432 self,
1433 block_size: int,
1431 - ) -> tuple[int, list[tuple[int, int, int]]]:
1434 + ) -> tuple[int, np.ndarray]:
1435 """
1436 Function used for register local xfer handler with local block_size or
1437 Remote block_size.
@@ -1461,9 +1464,8 @@ class NixlBaseConnectorWorker:
1464 # remote has been seen. Currently we always register 4 regions
1465 # because local descs are created before knowing the remote TP.
1466 logger.debug("Registering local Mamba descriptors (4 regions/layer)")
1464 - blocks_data.extend(
1465 - self._build_mamba_local(local_base_addresses, block_size_ratio)
1466 - )
1467 + mamba = self._build_mamba_local(local_base_addresses, block_size_ratio)
1468 + blocks_data = np.concatenate([blocks_data, mamba])
1469
1470 descs = self.nixl_wrapper.get_xfer_descs(blocks_data, self.nixl_memory_type)
1471 # NIXL_INIT_AGENT to be used for preparations of local descs.
@@ -1650,13 +1652,8 @@ class NixlBaseConnectorWorker:
1652 engine_id,
1653 remote_tp_rank,
1654 )
1653 - blocks_data.extend(
1654 - self._build_mamba_remote(
1655 - nixl_agent_meta,
1656 - tp_ratio,
1657 - transfer_info,
1658 - )
1659 - )
1655 + mamba = self._build_mamba_remote(nixl_agent_meta, tp_ratio, transfer_info)
1656 + blocks_data = np.concatenate([blocks_data, mamba])
1657
1658 # Register with NIXL.
1659 descs = self.nixl_wrapper.get_xfer_descs(blocks_data, self.nixl_memory_type)