[NIXL] Prepare KV transfer descriptors as NIXL 1.5 strided lists (#61032)

Signed-off-by: Nick Hill <nickhill123@gmail.com>
Co-authored-by: Ilia Yastrebov <iyastrebov@nvidia.com>
This commit is contained in:
Nick Hill
2026-10-14 18:07:24 +02:00
committed by GitHub
co-authored by Ilia Yastrebov
parent 9e2b579aab
commit eaf25f675a
11 changed files with 343 additions and 227 deletions
+4 -2
View File
@@ -385,7 +385,7 @@ COPY --from=build_vllm ${COMMON_WORKDIR}/vllm/vllm/v1 /vllm_v1
# NIXL/UCX build stages
FROM base AS build_nixl
ARG NIXL_BRANCH="231d56753047c989062a5cb2ac703a1ad761c7d2"
ARG NIXL_BRANCH="1683cf3b7f3d11674c03c5e861cea22339876c96"
ARG NIXL_REPO="https://github.com/ai-dynamo/nixl.git"
ARG UCX_BRANCH="96e58a16039f6d7d213bc967b8069238742c5194"
ARG UCX_REPO="https://github.com/openucx/ucx.git"
@@ -461,6 +461,8 @@ RUN --mount=type=cache,target=/root/.cache/ccache \
# so configure the ROCm wheel variant directly through Meson.
# Exclude libcore and libpull from auditwheel: transitive dependencies
# that are not shipped in the wheel and vary across base images.
# Tag manylinux_2_35 to match the Ubuntu 22.04 base: NIXL >= 1.5 links
# GLIBCXX_3.4.30 symbols, which manylinux_2_34 does not allow.
RUN cd /opt/nixl && \
./contrib/tomlutil.py --wheel-name nixl-rocm pyproject.toml && \
CC="ccache gcc" CXX="ccache g++" \
@@ -476,7 +478,7 @@ RUN cd /opt/nixl && \
--exclude 'libcore*' \
--exclude 'libpull*' \
/tmp/nixl_wheels/nixl_rocm*.whl \
--plat manylinux_2_34_$(uname -m) \
--plat manylinux_2_35_$(uname -m) \
--wheel-dir /tmp/nixl_wheels/repaired && \
./contrib/wheel_add_ucx_plugins.py \
--ucx-plugins-dir ${UCX_HOME}/lib/ucx \
+4 -2
View File
@@ -382,7 +382,7 @@ COPY --from=build_vllm ${COMMON_WORKDIR}/vllm/vllm/v1 /vllm_v1
# NIXL/UCX build stages
FROM base AS build_nixl
ARG NIXL_BRANCH="231d56753047c989062a5cb2ac703a1ad761c7d2"
ARG NIXL_BRANCH="1683cf3b7f3d11674c03c5e861cea22339876c96"
ARG NIXL_REPO="https://github.com/ai-dynamo/nixl.git"
ARG UCX_BRANCH="96e58a16039f6d7d213bc967b8069238742c5194"
ARG UCX_REPO="https://github.com/openucx/ucx.git"
@@ -459,6 +459,8 @@ RUN --mount=type=cache,target=/root/.cache/ccache \
# so configure the ROCm wheel variant directly through Meson.
# Exclude libcore and libpull from auditwheel: transitive dependencies
# that are not shipped in the wheel and vary across base images.
# Tag manylinux_2_35 to match the Ubuntu 22.04 base: NIXL >= 1.5 links
# GLIBCXX_3.4.30 symbols, which manylinux_2_34 does not allow.
RUN cd /opt/nixl && \
./contrib/tomlutil.py --wheel-name nixl-rocm pyproject.toml && \
CC="ccache gcc" CXX="ccache g++" \
@@ -474,7 +476,7 @@ RUN cd /opt/nixl && \
--exclude 'libcore*' \
--exclude 'libpull*' \
/tmp/nixl_wheels/nixl_rocm*.whl \
--plat manylinux_2_34_$(uname -m) \
--plat manylinux_2_35_$(uname -m) \
--wheel-dir /tmp/nixl_wheels/repaired && \
./contrib/wheel_add_ucx_plugins.py \
--ucx-plugins-dir ${UCX_HOME}/lib/ucx \
@@ -83,6 +83,7 @@ from .utils import (
create_request,
create_scheduler,
create_vllm_config,
expand_strided_descs,
make_kv_cache_config,
maybe_update_block_size,
)
@@ -159,7 +160,9 @@ class FakeNixlWrapper:
def get_xfer_descs(self, blocks_data, memory_type: str) -> list:
return [str(uuid.uuid4()) for _ in blocks_data]
def prep_xfer_dlist(self, agent_name: str, descs: list) -> int:
def prep_xfer_dlist(
self, agent_name: str, descs, mem_type: str | None = None
) -> int:
return uuid.uuid4().int
def get_agent_metadata(self) -> bytes:
@@ -554,7 +557,7 @@ class FakeNixlConnectorWorker(NixlConnectorWorker):
self.kv_cache_layout = kv_cache_layout
# Mock register_kv_caches attributes needed for tests that do not call it.
self.src_xfer_handles_by_block_size = {self.block_size: 1}
self.src_blocks_data = np.empty((0, 3), dtype=np.uint64)
self.src_blocks_data = np.empty((0, 5), dtype=np.uint64)
rep_spec = self.kv_cache_config.kv_cache_groups[0].kv_cache_spec
test_shape = compute_layer_kv_cache_shape_bytes(rep_spec, 1)
self.transfer_topo = TransferTopology(
@@ -929,11 +932,12 @@ class TestNixlHandshake:
worker.block_len_per_layer = [4096 * worker.block_size]
worker.num_blocks = 1
worker.dst_num_blocks[worker.engine_id] = worker.num_blocks
block_len = worker.block_len_per_layer[0]
worker.src_blocks_data = np.array(
[(0, worker.block_len_per_layer[0], worker.tp_rank)],
[(0, block_len, worker.tp_rank, block_len, worker.num_blocks)],
dtype=np.uint64,
)
worker.num_descs = len(worker.src_blocks_data)
worker.num_descs = int(worker.src_blocks_data[:, 4].sum())
def check_handshake(remote_tp_size: int):
tp_ratio = remote_tp_size // local_tp_size
@@ -1333,7 +1337,7 @@ class TestNixlHandshake:
)
assert worker._build_fa_remote(plan, meta, block_size_ratio=1).tolist() == [
[0x1000 + local_block_len, local_block_len, 0]
[0x1000 + local_block_len, local_block_len, 0, remote_block_len, 1]
]
@patch(
@@ -1371,12 +1375,12 @@ class TestNixlHandshake:
worker.dst_num_blocks[worker.engine_id] = worker.num_blocks
worker.src_blocks_data = np.array(
[
(0, fa_len, worker.tp_rank),
(0, idx_len, worker.tp_rank),
(0, fa_len, worker.tp_rank, fa_len, worker.num_blocks),
(0, idx_len, worker.tp_rank, idx_len, worker.num_blocks),
],
dtype=np.uint64,
)
worker.num_descs = len(worker.src_blocks_data)
worker.num_descs = int(worker.src_blocks_data[:, 4].sum())
# D_TP=2, P_TP=1 -> tp_ratio=2. SPLIT region scales by tp_ratio;
# REPLICATE region is unchanged.
@@ -2142,21 +2146,11 @@ def test_mixed_memory_local_descriptors_split_by_memory_type():
worker._desc_pos_by_block_size = {}
worker._dram_src_handles_by_block_size = {}
worker.nixl_memory_type = "VRAM"
# One dense run of two 10-byte blocks per region: (addr, len, dev, stride, count).
worker._build_fa_local = MagicMock( # type: ignore[method-assign]
return_value=np.array(
[
[100, 10, 3],
[110, 10, 3],
[200, 10, 3],
[210, 10, 3],
]
)
return_value=np.array([[100, 10, 3, 10, 2], [200, 10, 3, 10, 2]])
)
worker.nixl_wrapper = MagicMock()
worker.nixl_wrapper.get_xfer_descs.side_effect = lambda blocks, memory_type: (
memory_type,
blocks,
)
worker.nixl_wrapper.prep_xfer_dlist.side_effect = [11, 22]
handle, blocks = worker.register_local_xfer_handler(worker.block_size)
@@ -2165,11 +2159,17 @@ def test_mixed_memory_local_descriptors_split_by_memory_type():
assert worker._dram_src_handles_by_block_size[worker.block_size] == 11
# Must stay an array: P_TP > D_TP splits call .tolist() on it.
assert isinstance(blocks, np.ndarray)
assert blocks[:, 2].tolist() == [0, 0, 3, 3]
memory_types = [
call.args[1] for call in worker.nixl_wrapper.get_xfer_descs.call_args_list
# DRAM runs are re-homed to device 0; the per-descriptor maps consumed by
# the mixed READ path still cover every block.
assert blocks[:, 2].tolist() == [0, 3]
assert worker._desc_is_dram_by_block_size[16].tolist() == [True, True, False, False]
assert worker._desc_pos_by_block_size[16].tolist() == [0, 1, 0, 1]
dlist_calls = worker.nixl_wrapper.prep_xfer_dlist.call_args_list
assert [call.kwargs["mem_type"] for call in dlist_calls] == ["DRAM", "VRAM"]
assert [call.args[1].tolist() for call in dlist_calls] == [
[[100, 10, 0, 10, 2]],
[[200, 10, 3, 10, 2]],
]
assert memory_types == ["DRAM", "VRAM"]
@pytest.fixture
@@ -2362,8 +2362,8 @@ def test_register_kv_caches(
This test verifies:
1. nixl_wrapper.get_reg_descs() is called with caches_data containing
tensor metadata
2. nixl_wrapper.get_xfer_descs() is called with blocks_data containing
block layout info
2. nixl_wrapper.prep_xfer_dlist() is called with strided block runs whose
expansion covers exactly the registered blocks
"""
block_size = maybe_update_block_size(16)
vllm_config = create_vllm_config(
@@ -2479,9 +2479,12 @@ def test_register_kv_caches(
assert size == raw.nbytes
assert base_addr == raw.data_ptr()
# Verify get_xfer_descs was called with blocks_data
assert mock_wrapper_instance.get_xfer_descs.called
blocks_data, _ = mock_wrapper_instance.get_xfer_descs.call_args[0]
# Verify the local dlist was prepared from one strided run per region;
# expand it to per-block windows for the layout-blind checks below.
assert mock_wrapper_instance.prep_xfer_dlist.called
_agent, block_runs = mock_wrapper_instance.prep_xfer_dlist.call_args[0]
assert block_runs.shape[1] == 5
blocks_data = expand_strided_descs(block_runs)
# Layout-blind contract: whatever regions the worker carves out,
# transferring "block b" must move exactly logical block b's bytes for
@@ -2515,6 +2518,7 @@ def test_register_kv_caches(
start for start, _len, _tp in blocks_data if owner[start] == 0
}
assert len(blocks_data) == num_blocks * len(base_addrs)
assert len(block_runs) == len(base_addrs)
assert connector.connector_worker.block_size == block_size
@@ -2578,14 +2582,14 @@ def test_register_packed_dsv4_mla_cache_as_single_region(
)
connector.register_kv_caches(dict(zip(layer_names, views)))
blocks_data, _ = wrapper.get_xfer_descs.call_args[0]
_agent, block_runs = wrapper.prep_xfer_dlist.call_args[0]
packed_block_len = num_layers * spec.page_size_bytes
assert connector.connector_worker.kv_caches_base_addr[
connector.connector_worker.engine_id
][0] == [raw.data_ptr()]
assert blocks_data.tolist() == [
[raw.data_ptr() + block_idx * packed_block_len, packed_block_len, 0]
for block_idx in range(num_blocks)
# One packed region -> one run of num_blocks packed rows.
assert block_runs.tolist() == [
[raw.data_ptr(), packed_block_len, 0, packed_block_len, num_blocks]
]
@@ -37,6 +37,7 @@ from vllm.v1.kv_cache_interface import (
from .utils import (
create_request,
create_vllm_config,
expand_strided_descs,
make_kv_cache_config,
make_nixl_scheduler,
maybe_update_block_size,
@@ -2201,7 +2202,8 @@ def test_nixl_keeps_device_block_count_with_hisparse_host_pool(kernel_block_size
)
for block in range(count)
]
assert worker.src_blocks_data[:, 0].tolist() == expected_addrs
local_descs = expand_strided_descs(worker.src_blocks_data)
assert local_descs[:, 0].tolist() == expected_addrs
@pytest.mark.cpu_test
@@ -2284,19 +2286,21 @@ def test_register_kv_caches_hybrid_mla_dual_purpose_regions():
assert worker.num_regions == 2 and worker.num_descs == 24
# Kernel-granularity block lens; TP-independent for MLA hybrids.
assert worker.block_len_per_layer == [unified_page // 3] * 2
# Split handles must replicate every FA descriptor (MLA isn't head-sharded).
assert worker._fa_desc_replicated(worker.num_descs) == [True] * 24
# FA descs: 2 regions x 12 kernel blocks, page stride = kernel page.
# Mamba descs: 2 regions x (3 conv sub-projections + 1 ssm) x 4 blocks.
assert worker.src_blocks_data.shape == (24 + 32, 3)
fa_descs = worker.src_blocks_data[:24]
# FA runs: one per region x 12 kernel blocks, page stride = block_stride.
# Mamba runs: 2 regions x (3 conv sub-projections + 1 ssm), 4 blocks each.
assert worker.src_blocks_data.shape == (2 + 8, 5)
fa_runs = worker.src_blocks_data[:2]
assert fa_runs[:, 1].tolist() == [unified_page // 3] * 2
assert fa_runs[:, 3].tolist() == [block_stride] * 2
assert fa_runs[:, 4].tolist() == [12] * 2
assert worker.src_blocks_data[2:, 4].tolist() == [4] * 8
expected_addrs = [
tensor.data_ptr() + block * block_stride
for tensor in tensors
for block in range(12)
]
fa_descs = expand_strided_descs(fa_runs)
assert fa_descs[:, 0].tolist() == expected_addrs
assert all(size == unified_page // 3 for size in fa_descs[:, 1])
worker.nixl_wrapper.register_memory.assert_called_once()
metadata = msgspec.msgpack.decode(
worker.xfer_handshake_metadata.agent_metadata_bytes,
@@ -37,7 +37,7 @@ from vllm.v1.kv_cache_interface import (
create_kv_cache_views,
)
from .utils import create_vllm_config
from .utils import create_vllm_config, expand_strided_descs
class _RecordingNixl:
@@ -65,10 +65,11 @@ class _RecordingNixl:
def get_xfer_descs(self, blocks_data, mem_type):
return blocks_data
def prep_xfer_dlist(self, agent, descs):
def prep_xfer_dlist(self, agent, descs, mem_type=None):
handle = self._next_handle
self._next_handle += 1
self.dlists[handle] = np.asarray(descs, dtype=np.uint64).reshape(-1, 3)
# Store per-block (addr, len, dev) rows so desc ids index directly.
self.dlists[handle] = expand_strided_descs(np.asarray(descs, dtype=np.uint64))
return handle
def add_remote_agent(self, metadata):
@@ -186,9 +187,8 @@ def test_packed_mla_pp1_push_peer_transfers_whole_rows(num_layers):
stride = num_layers * 128
worker, raw = _make_packed_mla_view_worker(layouts, stride)
assert worker._registered_descs == [[(raw.data_ptr(), raw.nbytes, 0, "")]]
assert worker.src_blocks_data.tolist() == [
[raw.data_ptr() + block * stride, stride, 0] for block in range(4)
]
# One packed region -> one run of 4 whole rows.
assert worker.src_blocks_data.tolist() == [[raw.data_ptr(), stride, 0, stride, 4]]
assert worker._transfer_layer_group_ids == ()
metadata = msgspec.msgpack.decode(
worker.xfer_handshake_metadata.agent_metadata_bytes, type=NixlAgentMetadata
@@ -248,7 +248,8 @@ def test_packed_mla_pp_pairs_asymmetric_strides_and_overlapping_layers(
1,
region_num_blocks=producer.dst_region_num_blocks[producer.engine_id],
)
assert producer.src_blocks_data[local_ids].tolist() == [
local_descs = expand_strided_descs(producer.src_blocks_data)
assert local_descs[local_ids].tolist() == [
[p_raw.data_ptr() + 192, 128, 0],
[p_raw.data_ptr() + 2 * 192, 64, 0],
[p_raw.data_ptr() + 128 + 3 * 192, 64, 0],
@@ -318,7 +319,8 @@ def test_local_descriptors_follow_each_region_pool_capacity():
descriptors = worker._build_fa_local([100, 1000], block_size_ratio=1)
assert descriptors[:, 0].tolist() == [100, 116, 1000, 1016, 1032]
# One (addr, len, dev, stride, count) run per region, sized to its pool.
assert descriptors.tolist() == [[100, 16, 0, 16, 2], [1000, 16, 0, 16, 3]]
def _register_overlaid_mla_worker(
@@ -479,7 +481,8 @@ def test_overlaid_transfer_groups_share_region_geometry(push_pp):
allocation.data_ptr() + block * block_stride for block in range(num_blocks)
]
num_desc_regions = 2 if push_pp else 1
assert worker.src_blocks_data[:, 0].tolist() == expected_addrs * num_desc_regions
local_descs = expand_strided_descs(worker.src_blocks_data)
assert local_descs[:, 0].tolist() == expected_addrs * num_desc_regions
assert worker.num_descs == num_blocks * num_desc_regions
assert (
worker.dst_region_num_blocks[worker.engine_id]
@@ -500,11 +503,12 @@ def _descriptor_geometry(registered) -> dict:
"""The geometry handed to NIXL, addressed relative to the allocation."""
worker = registered.worker
base = registered.allocation.data_ptr()
descs = expand_strided_descs(worker.src_blocks_data)
return {
"block_len_per_layer": list(worker.block_len_per_layer),
"block_stride_per_layer": list(worker.block_stride_per_layer),
"desc_offsets": [addr - base for addr in worker.src_blocks_data[:, 0].tolist()],
"desc_lens": worker.src_blocks_data[:, 1].tolist(),
"desc_offsets": [addr - base for addr in descs[:, 0].tolist()],
"desc_lens": descs[:, 1].tolist(),
}
@@ -1365,10 +1369,11 @@ def test_csa_linear_ple_descriptor_is_not_split():
bases = [0x10000, 0x20000, 0x30000, 0x40000]
attention = worker._build_fa_local(bases, block_size_ratio=1)
mamba = worker._build_mamba_local(bases)
assert attention.shape == (8, 3)
assert mamba.shape == (10, 3)
assert mamba[-2:, 0].tolist() == [0x10000, 0x10100]
assert mamba[-2:, 1].tolist() == [256, 256]
# One (addr, len, dev, stride, count) run per region / sub-region.
assert attention.shape == (4, 5)
assert mamba.shape == (5, 5)
# PLE run: 2 whole 256-byte pages at 0x10000, 0x10100.
assert mamba[-1].tolist() == [0x10000, 256, 0, 256, 2]
mapping = TPMapping(
source_ranks_per_group=((0,), (0,), (0, 1), (0, 1)),
@@ -1380,14 +1385,15 @@ def test_csa_linear_ple_descriptor_is_not_split():
worker._build_local_splits_from_plan(
mapping,
np.concatenate([attention, mamba]),
num_fa_descs=len(attention),
num_fa_descs=int(attention[:, 4].sum()),
)
)
assert len(splits) == 2
assert [row[1] for row in splits[0][-2:]] == [256, 256]
assert [row[1] for row in splits[1][-2:]] == [256, 256]
assert splits[0][-3][1] * 2 == mamba[-3, 1]
assert splits[1][-3][1] * 2 == mamba[-3, 1]
for split in splits:
# The PLE run is copied whole to every source rank, while the sharded
# SSM run right before it is halved per source.
assert split[-1].tolist() == mamba[-1].tolist()
assert split[-2, 1] * 2 == mamba[-2, 1]
@pytest.mark.cpu_test
@@ -1424,9 +1430,9 @@ def test_csa_linear_remote_ple_is_copied_whole():
tp_ratio=-2,
transfer_info=SimpleNamespace(remote_physical_blocks_per_logical=1),
)
assert descriptors.shape == (10, 3)
assert descriptors[-2:, 0].tolist() == [0x10000, 0x10100]
assert descriptors[-2:, 1].tolist() == [256, 256]
assert descriptors.shape == (5, 5)
# PLE run: 2 whole 256-byte pages at 0x10000, 0x10100.
assert descriptors[-1].tolist() == [0x10000, 256, 0, 256, 2]
metadata.ple_block_len = 128
with pytest.raises(ValueError, match="PLE pages require identical"):
@@ -1451,8 +1457,8 @@ def test_csa_linear_ple_page_wider_than_the_shared_region_page():
bases = [0x10000, 0x20000, 0x30000, 0x40000]
mamba = worker._build_mamba_local(bases)
assert mamba[-2:, 0].tolist() == [0x10000, 0x10000 + 384]
assert mamba[-2:, 1].tolist() == [384, 384]
# PLE run: 2 whole 384-byte pages at 0x10000, 0x10000 + 384.
assert mamba[-1].tolist() == [0x10000, 384, 0, 384, 2]
metadata = msgspec.msgpack.decode(
worker.xfer_handshake_metadata.agent_metadata_bytes, type=NixlAgentMetadata
@@ -60,7 +60,7 @@ from vllm.distributed.kv_transfer.kv_connector.v1.nixl.utils import (
from vllm.v1.kv_cache_interface import FullAttentionSpec
from vllm.v1.outputs import KVConnectorOutput
from .utils import create_request, make_nixl_push_scheduler
from .utils import create_request, expand_strided_descs, make_nixl_push_scheduler
# ----------------------------------------------------------------- #
# Helpers / fakes #
@@ -2008,8 +2008,13 @@ def test_layer_descriptors_pair_layers_across_asymmetric_pp_split(
assert consumer.kv_caches_base_addr == [0xB000, 0xC000, 0xC000]
plan = TPMapping(((0,), (0,)), (0,), {0: 0}, 0)
local_descs = worker._build_fa_local([0x1000, 0x2000], block_size_ratio=1)
remote_descs = worker._build_fa_remote(plan, consumer, block_size_ratio=1)
# Builders emit one strided run per region; expand to per-block descriptors.
local_descs = expand_strided_descs(
worker._build_fa_local([0x1000, 0x2000], block_size_ratio=1)
)
remote_descs = expand_strided_descs(
worker._build_fa_remote(plan, consumer, block_size_ratio=1)
)
local_counts = [local_num_blocks[i] for i in worker._transfer_layer_region_indices]
local_ids = worker._compute_desc_ids(
[[1, 2], [3]], 4, None, 1, region_num_blocks=local_counts
@@ -75,7 +75,9 @@ class FakeNixlWrapper:
def get_xfer_descs(self, blocks_data, memory_type: str) -> list:
return [str(uuid.uuid4()) for _ in blocks_data]
def prep_xfer_dlist(self, agent_name: str, descs: list) -> int:
def prep_xfer_dlist(
self, agent_name: str, descs, mem_type: str | None = None
) -> int:
return uuid.uuid4().int
def get_agent_metadata(self) -> bytes:
+36 -36
View File
@@ -145,9 +145,8 @@ def _make_mock_worker_for_splits(group_spec_types):
"""Build a mock NixlConnectorWorker with _group_spec_types for split tests.
No per-region replicate flags are configured (``block_len_per_layer`` empty
and ``num_regions == 0``), so ``_fa_desc_replicated`` takes its early-return
path and treats every FA descriptor as SPLIT, matching the legacy behavior
these tests assert.
and ``num_regions == 0``), so ``_build_local_splits_from_plan`` treats every
FA descriptor as SPLIT, matching the legacy behavior these tests assert.
"""
worker = object.__new__(NixlConnectorWorker)
worker._group_spec_types = group_spec_types
@@ -174,11 +173,9 @@ class TestBuildSrcSplitHandles:
)
worker = _make_mock_worker_for_splits((FullAttentionSpec,))
src_blocks_data = np.array(
[(0x2000 + i * 1024, 1024, 0) for i in range(8)],
dtype=np.uint64,
)
num_descs = len(src_blocks_data)
# One region of 8 dense 1024-byte blocks as a single strided run.
src_blocks_data = np.array([(0x2000, 1024, 0, 1024, 8)], dtype=np.uint64)
num_descs = 8
splits = list(
worker._build_local_splits_from_plan(
plan,
@@ -189,9 +186,10 @@ class TestBuildSrcSplitHandles:
assert len(splits) == remote_tp_size
for handle in splits:
assert len(handle) == len(src_blocks_data)
for _, length, _ in handle:
assert length == 1024 // remote_tp_size
assert handle.shape == src_blocks_data.shape
# The head slice shrinks the length; block stride and count are kept.
assert handle[:, 1].tolist() == [1024 // remote_tp_size]
assert handle[:, 3:].tolist() == [[1024, 8]]
class TestMambaPlanSplitHandles:
@@ -209,12 +207,12 @@ class TestMambaPlanSplitHandles:
)
worker = _make_mock_worker_for_splits((FullAttentionSpec, MambaSpec))
# 2 FA descs + 1 SSM desc
# 2 FA runs + 1 SSM run of one block each: (addr, len, dev, stride, count)
src_blocks_data = np.array(
[
(1000, 200, 0), # FA desc 0
(2000, 200, 0), # FA desc 1
(3000, 400, 0), # SSM desc 0
(1000, 200, 0, 200, 1), # FA desc 0
(2000, 200, 0, 200, 1), # FA desc 1
(3000, 400, 0, 400, 1), # SSM desc 0
],
dtype=np.uint64,
)
@@ -224,14 +222,22 @@ class TestMambaPlanSplitHandles:
assert len(splits) == 2 # 2 source ranks
# Rank 0 (FA source, p_idx=0):
# FA: chunk=200//1=200, slot=0 → (1000, 200, 0), (2000, 200, 0)
# SSM: chunk=400//2=200, idx=0 → (3000, 200, 0)
assert splits[0] == [(1000, 200, 0), (2000, 200, 0), (3000, 200, 0)]
# FA: chunk=200//1=200, slot=0 → (1000, 200), (2000, 200)
# SSM: chunk=400//2=200, idx=0 → (3000, 200); block stride stays 400
assert splits[0].tolist() == [
[1000, 200, 0, 200, 1],
[2000, 200, 0, 200, 1],
[3000, 200, 0, 400, 1],
]
# Rank 1 (not FA source, p_idx=1):
# FA: chunk=200//1=200, slot=0 (skip_fa) → (1000, 200, 0), (2000, 200, 0)
# SSM: chunk=400//2=200, idx=1 → (3200, 200, 0)
assert splits[1] == [(1000, 200, 0), (2000, 200, 0), (3200, 200, 0)]
# FA: chunk=200//1=200, slot=0 (skip_fa) → (1000, 200), (2000, 200)
# SSM: chunk=400//2=200, idx=1 → (3200, 200); block stride stays 400
assert splits[1].tolist() == [
[1000, 200, 0, 200, 1],
[2000, 200, 0, 200, 1],
[3200, 200, 0, 400, 1],
]
def test_hetero_block_size_splits(self):
"""With a block-size ratio, single-source FA sub-block descs pass
@@ -244,14 +250,13 @@ class TestMambaPlanSplitHandles:
)
worker = _make_mock_worker_for_splits((FullAttentionSpec, MambaSpec))
# 2 FA blocks x ratio 2 sub-blocks + 1 SSM desc (never expanded).
# 2 FA blocks x ratio 2 sub-blocks (one run of 2 per block) + 1 SSM desc
# (never expanded).
src_blocks_data = np.array(
[
(1000, 100, 0),
(1100, 100, 0),
(2000, 100, 0),
(2100, 100, 0),
(3000, 400, 0),
(1000, 100, 0, 100, 2),
(2000, 100, 0, 100, 2),
(3000, 400, 0, 400, 1),
],
dtype=np.uint64,
)
@@ -259,14 +264,9 @@ class TestMambaPlanSplitHandles:
splits = list(worker._build_local_splits_from_plan(plan, src_blocks_data, 4, 2))
assert len(splits) == 2
fa_passthrough = [
(1000, 100, 0),
(1100, 100, 0),
(2000, 100, 0),
(2100, 100, 0),
]
assert splits[0] == fa_passthrough + [(3000, 200, 0)]
assert splits[1] == fa_passthrough + [(3200, 200, 0)]
fa_passthrough = [[1000, 100, 0, 100, 2], [2000, 100, 0, 100, 2]]
assert splits[0].tolist() == fa_passthrough + [[3000, 200, 0, 400, 1]]
assert splits[1].tolist() == fa_passthrough + [[3200, 200, 0, 400, 1]]
def test_hetero_block_size_head_sharded_asserts(self):
"""Head-sharded FA reads (multiple FA sources) are incompatible with
@@ -280,7 +280,7 @@ class TestMambaPlanSplitHandles:
worker = _make_mock_worker_for_splits((FullAttentionSpec, MambaSpec))
src_blocks_data = np.array(
[(1000, 100, 0), (1100, 100, 0), (3000, 400, 0)],
[(1000, 100, 0, 100, 2), (3000, 400, 0, 400, 1)],
dtype=np.uint64,
)
+16
View File
@@ -8,6 +8,7 @@ from itertools import chain, count
from typing import Any, Literal
from unittest.mock import Mock
import numpy as np
import torch
from vllm import SamplingParams
@@ -682,3 +683,18 @@ def make_moriio_writer(fake_worker: Any) -> Any:
writer._defer_timeout = 60.0
writer.ensure_worker_started = lambda: None
return writer
def expand_strided_descs(strided_descs: np.ndarray) -> np.ndarray:
"""Expand Nx5 (addr, len, dev, stride, count) rows to one row per block.
Mirrors how NIXL indexes a strided descriptor list, so desc ids computed
by the connector can be resolved to (addr, len, dev) byte ranges.
"""
strided_descs = np.asarray(strided_descs, dtype=np.uint64).reshape(-1, 5)
counts = strided_descs[:, 4].astype(np.intp)
starts = np.cumsum(strided_descs[:, 4]) - strided_descs[:, 4]
out = np.repeat(strided_descs[:, :3], counts, axis=0)
offsets = np.arange(len(out), dtype=np.uint64) - np.repeat(starts, counts)
out[:, 0] += offsets * np.repeat(strided_descs[:, 3], counts)
return out
@@ -65,7 +65,11 @@ from vllm.distributed.kv_transfer.kv_connector.v1.ssm_conv_transfer_utils import
MambaConvSplitInfo,
derive_mamba_conv_split,
)
from vllm.distributed.nixl_utils import NixlWrapper, nixl_agent_config
from vllm.distributed.nixl_utils import (
NixlWrapper,
check_nixl_version,
nixl_agent_config,
)
from vllm.distributed.parallel_state import (
get_pcp_group,
get_tensor_model_parallel_rank,
@@ -277,20 +281,23 @@ class NixlBaseConnectorWorker:
src_blocks_data: np.ndarray,
num_fa_descs: int,
block_size_ratio: int = 1,
) -> Iterator[list[tuple[int, int, int]]]:
) -> Iterator[np.ndarray]:
"""Build split handle data for P_TP > D_TP scenario.
num_fa_descs is the boundary between FA and SSM descriptors.
Split counts are derived from source_ranks_per_group lengths.
FA uses rank_to_attention_slot for the slot offset;
SSM uses the rank's positional index.
Each Nx5 strided desc covers one region, so slicing it slices every block.
"""
fa_idx = next(
i for i, t in enumerate(self._group_spec_types) if _is_attention_spec(t)
)
fa_num_splits = len(plan.source_ranks_per_group[fa_idx])
has_ssm_descs = num_fa_descs < len(src_blocks_data)
starts = self._strided_desc_starts(src_blocks_data)
num_descs = int(src_blocks_data[:, 4].sum())
has_ssm_descs = num_fa_descs < num_descs
ssm_idx = next(
(i for i, t in enumerate(self._group_spec_types) if _is_ssm_spec(t)),
None,
@@ -300,38 +307,34 @@ class NixlBaseConnectorWorker:
if has_ssm_descs and ssm_idx is not None
else 0
)
# Per-FA-descriptor replicate flag, in _build_fa_local emission order.
fa_desc_replicated = self._fa_desc_replicated(num_fa_descs, block_size_ratio)
sharded_desc_end = len(src_blocks_data) - (
sharded_desc_end = num_descs - (
self._logical_num_blocks if self._ple_region_index is not None else 0
)
assert num_fa_descs <= sharded_desc_end
assert block_size_ratio == 1 or fa_num_splits == 1 or all(fa_desc_replicated), (
# REPLICATE (MLA) descs are read whole on every rank; SPLIT (full-attn)
# descs take this rank's head slice. SSM descs before the PLE pages are
# sharded by the rank's positional index; PLE pages are copied whole.
is_fa = starts < num_fa_descs
fa_replicated = self._fa_desc_replicated(
starts[is_fa], num_fa_descs, block_size_ratio
)
assert block_size_ratio == 1 or fa_num_splits == 1 or fa_replicated.all(), (
"Head-sharded attention reads with P_TP > D_TP and heterogeneous "
"block sizes are not supported"
)
src_blocks_list = src_blocks_data.tolist()
fa_split = is_fa.copy()
fa_split[is_fa] = ~fa_replicated
ssm_split = ~is_fa & (starts < sharded_desc_end)
for p_idx, p_rank in enumerate(plan.all_source_ranks):
fa_slot = plan.rank_to_attention_slot.get(p_rank, 0)
handle: list[tuple[int, int, int]] = []
for j, (addr, local_len, dev) in enumerate(src_blocks_list):
if j < num_fa_descs:
if fa_desc_replicated[j]:
# REPLICATE (MLA): whole block written on every rank.
handle.append((addr, local_len, dev))
else:
# SPLIT (full-attn): this rank's head slice.
chunk = local_len // fa_num_splits
handle.append((addr + fa_slot * chunk, chunk, dev))
elif j < sharded_desc_end:
chunk = local_len // ssm_num_splits
handle.append((addr + p_idx * chunk, chunk, dev))
else:
handle.append((addr, local_len, dev))
handle = src_blocks_data.copy()
handle[fa_split, 1] //= fa_num_splits
handle[fa_split, 0] += fa_slot * handle[fa_split, 1]
if ssm_num_splits:
handle[ssm_split, 1] //= ssm_num_splits
handle[ssm_split, 0] += p_idx * handle[ssm_split, 1]
yield handle
def _needs_split_local_xfer_handles(self, tp_ratio: int, plan: TPMapping) -> bool:
@@ -346,25 +349,30 @@ class NixlBaseConnectorWorker:
return tp_ratio < 0 and (not self.use_mla or len(plan.all_source_ranks) > 1)
def _fa_desc_replicated(
self, num_fa_descs: int, block_size_ratio: int = 1
) -> list[bool]:
"""Per-FA-descriptor replicate flag, in _build_fa_local emission order:
region-major, ``region_num_blocks[i] * block_size_ratio`` descs per
region. Length ``num_fa_descs``.
self,
desc_starts: np.ndarray,
num_fa_descs: int,
block_size_ratio: int = 1,
) -> np.ndarray:
"""Replicate flag per FA strided desc, given each one's flat block index.
_build_fa_local emits regions in order, with
``region_num_blocks[i] * block_size_ratio`` blocks per region. A strided
desc never spans regions, so the region owning its first block decides.
"""
assert self.transfer_topo is not None
n_regions = len(self.block_len_per_layer)
if n_regions == 0 or self.num_regions == 0:
return [False] * num_fa_descs
return np.zeros(len(desc_starts), dtype=bool)
region_indices = self._transfer_layer_region_indices or range(n_regions)
flags: list[bool] = []
for i in region_indices:
num_descs = self.region_num_blocks[i] * block_size_ratio
flags.extend([self._is_region_replicated(i)] * num_descs)
assert len(flags) == num_fa_descs, (
f"FA desc flags {len(flags)} != num_fa_descs {num_fa_descs}"
region_ends = np.cumsum(
[self.region_num_blocks[i] * block_size_ratio for i in region_indices]
)
return flags
assert region_ends[-1] == num_fa_descs, (
f"FA region blocks {region_ends[-1]} != num_fa_descs {num_fa_descs}"
)
replicated = np.array([self._is_region_replicated(i) for i in region_indices])
return replicated[np.searchsorted(region_ends, desc_starts, side="right")]
def _is_region_replicated(self, region_idx: int) -> bool:
"""Whether region ``region_idx`` is transferred REPLICATE vs SPLIT.
@@ -512,6 +520,8 @@ class NixlBaseConnectorWorker:
if nixl_wrapper_cls is None:
logger.error("NIXL is not available")
raise RuntimeError("NIXL is not available")
# Descriptor lists are prepared in NIXL's strided (Nx5) form.
check_nixl_version("1.5.0")
logger.info("Initializing NIXL wrapper")
logger.info("Initializing NIXL worker %s", engine_id)
@@ -1492,7 +1502,6 @@ class NixlBaseConnectorWorker:
if isinstance(layer_spec, MambaSpec)
else logical_num_blocks * self._physical_blocks_per_logical_kv_block
)
base_addr = cache.data_ptr()
is_mla_region = isinstance(
layer_spec, (MLAAttentionSpec, SlidingWindowMLASpec)
)
@@ -1827,7 +1836,7 @@ class NixlBaseConnectorWorker:
def _build_mamba_local(self, base_addresses: list[int]) -> np.ndarray:
"""Build desc regions (conv sub-projections + ssm) per layer for
local mamba blocks with DS conv layout, as an Nx3 uint64 array.
local mamba blocks with DS conv layout, as Nx5 uint64 strided descs.
A Mamba block interleaves conv and SSM state, which crucially differ in
size, so the two are indexed as separate sub-regions. Attention blocks
@@ -1865,28 +1874,32 @@ class NixlBaseConnectorWorker:
num_blocks = self._logical_num_blocks
physical_per_logical = self._physical_blocks_per_logical_kv_block
device_id = self.device_id
block_arange = np.arange(num_blocks, dtype=np.uint64)
parts: list[np.ndarray] = []
strided_descs: list[tuple[int, int, int, int, int]] = []
region_indices = self._ssm_region_indices or range(len(base_addresses))
for i in region_indices:
base_addr = base_addresses[i]
block_stride = self.block_stride_per_layer[i] * physical_per_logical
blk_addrs = base_addr + block_arange * block_stride
for off, sz in conv_offsets:
parts.append(self._stack_descs(blk_addrs + off, sz, device_id))
strided_descs.append(
(base_addr + off, sz, device_id, block_stride, num_blocks)
)
# SSM temporal state follows the conv state.
parts.append(self._stack_descs(blk_addrs + conv_size, ssm_size, device_id))
strided_descs.append(
(base_addr + conv_size, ssm_size, device_id, block_stride, num_blocks)
)
if (region_index := self._ple_region_index) is not None:
block_len = self._ple_page_len() * physical_per_logical
block_stride = (
self.block_stride_per_layer[region_index] * physical_per_logical
)
block_addrs = base_addresses[region_index] + block_arange * block_stride
parts.append(self._stack_descs(block_addrs, block_len, self.device_id))
base_addr = base_addresses[region_index]
strided_descs.append(
(base_addr, block_len, device_id, block_stride, num_blocks)
)
return np.concatenate(parts)
return self._pack_strided_descs(strided_descs)
def _build_mamba_remote(
self,
@@ -1896,7 +1909,7 @@ class NixlBaseConnectorWorker:
) -> np.ndarray:
"""Build remote desc regions (conv sub-projections + ssm) per layer.
For hetero-TP, each D rank reads only its sub-projection slice from
the P rank. Returns an Nx3 uint64 array."""
the P rank. Returns Nx5 uint64 strided descs."""
assert nixl_agent_meta.kv_caches_base_addr, (
"Remote KV cache base addresses must not be empty."
)
@@ -1916,9 +1929,8 @@ class NixlBaseConnectorWorker:
remote_physical_per_logical = transfer_info.remote_physical_blocks_per_logical
num_blocks = nixl_agent_meta.num_blocks // remote_physical_per_logical
device_id = nixl_agent_meta.device_id
block_arange = np.arange(num_blocks, dtype=np.uint64)
parts: list[np.ndarray] = []
strided_descs: list[tuple[int, int, int, int, int]] = []
# NOTE (ZhanqiuHu): use per-layer block_lens[i], not [0], in case
# block lengths vary across layers (e.g. MLA).
region_indices = self._ssm_region_indices or range(
@@ -1929,12 +1941,15 @@ class NixlBaseConnectorWorker:
block_stride = (
nixl_agent_meta.block_strides[i] * remote_physical_per_logical
)
blk_addrs = base_addr + block_arange * block_stride
for off, sz in conv_offsets:
parts.append(self._stack_descs(blk_addrs + off, sz, device_id))
strided_descs.append(
(base_addr + off, sz, device_id, block_stride, num_blocks)
)
# SSM temporal state is also TP-sharded on the heads dimension.
ssm_addrs = blk_addrs + conv_size_remote + local_offset * ssm_read_size
parts.append(self._stack_descs(ssm_addrs, ssm_read_size, device_id))
ssm_addr = base_addr + conv_size_remote + local_offset * ssm_read_size
strided_descs.append(
(ssm_addr, ssm_read_size, device_id, block_stride, num_blocks)
)
if (region_index := self._ple_region_index) is not None:
local_block_len = (
@@ -1954,52 +1969,85 @@ class NixlBaseConnectorWorker:
nixl_agent_meta.block_strides[region_index]
* remote_physical_per_logical
)
block_addrs = (
nixl_agent_meta.kv_caches_base_addr[region_index]
+ block_arange * remote_block_stride
addr = nixl_agent_meta.kv_caches_base_addr[region_index]
strided_descs.append(
(addr, remote_block_len, device_id, remote_block_stride, num_blocks)
)
parts.append(self._stack_descs(block_addrs, remote_block_len, device_id))
return np.concatenate(parts)
return self._pack_strided_descs(strided_descs)
@staticmethod
def _stack_descs(addrs: np.ndarray, length: int, device_id: int) -> np.ndarray:
out = np.empty((addrs.shape[0], 3), dtype=np.uint64)
out[:, 0] = addrs
out[:, 1] = length
out[:, 2] = device_id
return out
def _pack_strided_descs(
strided_descs: list[tuple[int, int, int, int, int]],
) -> np.ndarray:
"""Pack (addr, len, dev_id, stride, count) rows into an Nx5 uint64 array.
Each row describes `count` blocks of `len` bytes whose starts are
`stride` bytes apart; NIXL >= 1.5 prepares this layout directly.
"""
return np.array(strided_descs, dtype=np.uint64).reshape(-1, 5)
@staticmethod
def _block_strided_descs(
base_addr: int,
block_len: int,
block_stride: int,
num_blocks: int,
block_size_ratio: int,
device_id: int,
) -> np.ndarray:
"""Nx5 strided descs for `num_blocks` blocks, each split into
`block_size_ratio` sub-blocks of `block_len` bytes.
Evenly spaced sub-blocks collapse into one strided desc. In a padded
block the sub-blocks abut only within the block, so each block gets
its own.
"""
if block_size_ratio == 1 or block_stride == block_size_ratio * block_len:
stride = block_stride // block_size_ratio
count = num_blocks * block_size_ratio
return np.array(
[[base_addr, block_len, device_id, stride, count]], dtype=np.uint64
)
strided_descs = np.empty((num_blocks, 5), dtype=np.uint64)
strided_descs[:, 0] = (
base_addr + np.arange(num_blocks, dtype=np.uint64) * block_stride
)
strided_descs[:, 1] = block_len
strided_descs[:, 2] = device_id
strided_descs[:, 3] = block_len
strided_descs[:, 4] = block_size_ratio
return strided_descs
@staticmethod
def _strided_desc_starts(strided_descs: np.ndarray) -> np.ndarray:
"""Flat block index of each strided desc's first block."""
counts = strided_descs[:, 4]
return np.cumsum(counts) - counts
def _build_fa_local(
self,
base_addresses: list[int],
block_size_ratio: int,
) -> np.ndarray:
"""Build local FA descriptors as an Nx3 uint64 array."""
"""Build local FA descriptors as Nx5 uint64 strided descs."""
assert self.transfer_topo is not None
assert base_addresses, "Local KV cache base addresses must not be empty."
device_id = self.device_id
region_indices = self._transfer_layer_region_indices or range(
len(base_addresses)
)
parts: list[np.ndarray] = []
for i in region_indices:
base_addr = base_addresses[i]
block_len = self.block_len_per_layer[i] // block_size_ratio
logical_blocks = np.repeat(
np.arange(self.region_num_blocks[i], dtype=np.uint64),
block_size_ratio,
)
split_offsets = np.tile(
np.arange(block_size_ratio, dtype=np.uint64),
parts = [
self._block_strided_descs(
base_addresses[i],
self.block_len_per_layer[i] // block_size_ratio,
self.block_stride_per_layer[i],
self.region_num_blocks[i],
block_size_ratio,
device_id,
)
addrs = (
base_addr
+ logical_blocks * self.block_stride_per_layer[i]
+ split_offsets * block_len
)
parts.append(self._stack_descs(addrs, block_len, device_id))
for i in region_indices
]
return np.concatenate(parts)
def _build_fa_remote(
@@ -2008,7 +2056,7 @@ class NixlBaseConnectorWorker:
nixl_agent_meta: NixlAgentMetadata,
block_size_ratio: int,
) -> np.ndarray:
"""Build remote FA descriptors as an Nx3 uint64 array."""
"""Build remote FA descriptors as Nx5 uint64 strided descs."""
assert self.transfer_topo is not None
assert nixl_agent_meta.kv_caches_base_addr, (
"Remote KV cache base addresses must not be empty."
@@ -2024,7 +2072,7 @@ class NixlBaseConnectorWorker:
] * len(nixl_agent_meta.kv_caches_base_addr)
device_id = nixl_agent_meta.device_id
block_strides = nixl_agent_meta.block_strides
parts: list[np.ndarray] = []
strided_descs: list[tuple[int, int, int, int, int]] = []
use_layer_name_routing = bool(self._transfer_layer_region_indices)
for i, base_addr in enumerate(nixl_agent_meta.kv_caches_base_addr):
local_region = (
@@ -2046,10 +2094,12 @@ class NixlBaseConnectorWorker:
)
local_block_len = local_block_len // num_reads
block_arange = np.arange(region_num_blocks[i], dtype=np.uint64)
addrs = base_addr + rank_offset + block_arange * block_strides[i]
parts.append(self._stack_descs(addrs, local_block_len, device_id))
return np.concatenate(parts)
addr = base_addr + rank_offset
count = region_num_blocks[i]
strided_descs.append(
(addr, local_block_len, device_id, block_strides[i], count)
)
return self._pack_strided_descs(strided_descs)
def register_local_xfer_handler(
self,
@@ -2072,13 +2122,13 @@ class NixlBaseConnectorWorker:
blocks_data = self._build_fa_local(local_base_addresses, block_size_ratio)
logger.debug(
"Created %s blocks for src engine %s and rank %s on device id %s",
len(blocks_data),
blocks_data[:, 4].sum(),
self.engine_id,
self.tp_rank,
self.device_id,
)
if self._has_mamba:
assert self.num_descs * block_size_ratio == len(blocks_data)
assert self.num_descs * block_size_ratio == blocks_data[:, 4].sum()
# TODO (ZhanqiuHu): For homogeneous TP (tp_ratio == 1), the 3-descs split
# is unnecessary — a single conv desc per block suffices. Consider
# adding a fast path that falls back to the standard 2-region
@@ -2104,7 +2154,7 @@ class NixlBaseConnectorWorker:
)
]
)
assert len(desc_is_dram) == len(blocks_data)
assert len(desc_is_dram) == blocks_data[:, 4].sum()
desc_pos = np.empty(len(desc_is_dram), dtype=np.int64)
dram_idx = np.where(desc_is_dram)[0]
vram_idx = np.where(~desc_is_dram)[0]
@@ -2112,28 +2162,32 @@ class NixlBaseConnectorWorker:
desc_pos[vram_idx] = np.arange(len(vram_idx), dtype=np.int64)
self._desc_is_dram_by_block_size[block_size] = desc_is_dram
self._desc_pos_by_block_size[block_size] = desc_pos
# DRAM descriptors are registered under CPU device 0.
blocks_data[desc_is_dram, 2] = 0
dram_blocks = blocks_data[dram_idx]
vram_blocks = blocks_data[vram_idx]
# A strided desc never spans regions, so its first block decides.
strided_is_dram = desc_is_dram[self._strided_desc_starts(blocks_data)]
blocks_data[strided_is_dram, 2] = 0
if self._skip_dram_xfer:
self._dram_src_handles_by_block_size[block_size] = None
else:
dram_descs = self.nixl_wrapper.get_xfer_descs(dram_blocks, "DRAM")
self._dram_src_handles_by_block_size[block_size] = (
self.nixl_wrapper.prep_xfer_dlist("NIXL_INIT_AGENT", dram_descs)
self.nixl_wrapper.prep_xfer_dlist(
"NIXL_INIT_AGENT",
blocks_data[strided_is_dram],
mem_type="DRAM",
)
)
descs = self.nixl_wrapper.get_xfer_descs(vram_blocks, self.nixl_memory_type)
return (
self.nixl_wrapper.prep_xfer_dlist("NIXL_INIT_AGENT", descs),
blocks_data,
device_handle = self.nixl_wrapper.prep_xfer_dlist(
"NIXL_INIT_AGENT",
blocks_data[~strided_is_dram],
mem_type=self.nixl_memory_type,
)
return device_handle, blocks_data
mem_type = self.region_mem_types[0]
descs = self.nixl_wrapper.get_xfer_descs(blocks_data, mem_type)
# NIXL_INIT_AGENT to be used for preparations of local descs.
return self.nixl_wrapper.prep_xfer_dlist("NIXL_INIT_AGENT", descs), blocks_data
handle = self.nixl_wrapper.prep_xfer_dlist(
"NIXL_INIT_AGENT", blocks_data, mem_type=self.region_mem_types[0]
)
return handle, blocks_data
def add_remote_agent(
self,
@@ -2350,23 +2404,23 @@ class NixlBaseConnectorWorker:
block_size_ratio,
):
if self._mixed_mem_types:
handle_data = np.asarray(handle_data, dtype=np.uint64)
desc_is_dram = self._desc_is_dram_by_block_size[remote_block_size]
strided_is_dram = desc_is_dram[
self._strided_desc_starts(handle_data)
]
if self._skip_dram_xfer:
dram_handle = None
else:
dram_descs = self.nixl_wrapper.get_xfer_descs(
handle_data[desc_is_dram], "DRAM"
)
dram_handle = self.nixl_wrapper.prep_xfer_dlist(
"NIXL_INIT_AGENT", dram_descs
"NIXL_INIT_AGENT",
handle_data[strided_is_dram],
mem_type="DRAM",
)
self._dram_src_handles_by_tp_ratio[split_key].append(dram_handle)
handle_data = handle_data[~desc_is_dram]
descs = self.nixl_wrapper.get_xfer_descs(
handle_data, self.nixl_memory_type
handle_data = handle_data[~strided_is_dram]
handle = self.nixl_wrapper.prep_xfer_dlist(
"NIXL_INIT_AGENT", handle_data, mem_type=self.nixl_memory_type
)
handle = self.nixl_wrapper.prep_xfer_dlist("NIXL_INIT_AGENT", descs)
self.src_xfer_handles_by_tp_ratio[split_key].append(handle)
### Register remote agent memory regions
@@ -2383,7 +2437,7 @@ class NixlBaseConnectorWorker:
)
logger.debug(
"Created %s blocks for dst engine %s with remote rank %s and local rank %s",
len(blocks_data),
blocks_data[:, 4].sum(),
engine_id,
remote_tp_rank,
self.tp_rank,
@@ -2402,9 +2456,10 @@ class NixlBaseConnectorWorker:
raise NotImplementedError(
"NIXL pull does not yet support a mixed-memory producer"
)
descs = self.nixl_wrapper.get_xfer_descs(blocks_data, remote_mem_types.pop())
self.dst_xfer_side_handles[engine_id][remote_tp_rank] = (
self.nixl_wrapper.prep_xfer_dlist(remote_agent_name, descs)
self.nixl_wrapper.prep_xfer_dlist(
remote_agent_name, blocks_data, mem_type=remote_mem_types.pop()
)
)
return remote_agent_name
+20
View File
@@ -1,6 +1,7 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
import functools
import importlib
import os
import sys
@@ -91,6 +92,24 @@ def is_nixl_available() -> bool:
return pkg in sys.modules or importlib.util.find_spec(pkg) is not None
@functools.cache
def check_nixl_version(min_version: str) -> None:
"""Raise if the installed NIXL package is older than ``min_version``."""
from importlib.metadata import packages_distributions, version
from packaging.version import Version
pkg = _get_nixl_package_name()
for dist in packages_distributions().get(pkg, ()):
installed = version(dist)
if Version(installed) < Version(min_version):
raise RuntimeError(
f"NIXL >= {min_version} is required, but {dist}=={installed} "
"is installed"
)
return
def alias_nixl_for_ray() -> None:
"""Let Ray's ``nixl._api`` import resolve to the ROCm implementation."""
if not current_platform.is_rocm() or "nixl._api" in sys.modules:
@@ -112,5 +131,6 @@ __all__ = [
"nixl_agent_config",
"nixlXferTelemetry",
"is_nixl_available",
"check_nixl_version",
"alias_nixl_for_ray",
]