[Perf][KV Connector][Mooncake] Pack hybrid/MLA KV into coalesced transfer regions (#57952)

Signed-off-by: staryxchen <staryxchen@tencent.com> Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Yifan Qiao <yifanqiao@inferact.ai>

Stary committed Sep 29, 2026 at 22:29 UTC faacc13565312d29e9596d182fc808b452a4508e
3 files changed +1386 -209
tests/v1/kv_connector/unit/test_mooncake_connector.py
+867 -167
@@ -23,6 +23,7 @@ from vllm.distributed.kv_transfer.kv_connector.v1.mooncake import (
23 mooncake_utils,
24 )
25 from vllm.distributed.kv_transfer.kv_connector.v1.mooncake.mooncake_connector import (
26 + _SHARED_REGION_GROUP_ID,
27 KVConnectorRole,
28 MooncakeConnector,
29 MooncakeConnectorMetadata,
@@ -34,7 +35,11 @@ from vllm.distributed.kv_transfer.kv_connector.v1.mooncake.mooncake_connector im
35 SendBlockMeta,
36 TransferRegion,
37 _align_transfer_regions,
38 + _block_ids_for_region,
39 + _coalesce_contiguous_transfer_regions,
40 _compute_sender_transfer_plan,
41 + _has_opaque_packed_row,
42 + _pp_mismatch_hides_packed_layers,
43 _validate_asymmetric_region_lengths,
44 get_mooncake_bootstrap_addr,
45 should_launch_bootstrap_server,
@@ -48,6 +53,7 @@ from vllm.v1.kv_cache_interface import (
53 KVCacheConfig,
54 KVCacheGroupSpec,
55 KVCacheLayout,
56 + MLAAttentionSpec,
57 )
58 from vllm.v1.request import RequestStatus
59
@@ -164,6 +170,34 @@ def _make_test_kv_cache_config() -> KVCacheConfig:
170 )
171
172
173 +def _make_packed_mla_kv_cache_config(
174 + num_blocks: int, *, num_groups: int = 1
175 +) -> KVCacheConfig:
176 + spec = MLAAttentionSpec(
177 + block_size=16,
178 + num_kv_heads=1,
179 + head_size=64,
180 + dtype=torch.uint8,
181 + )
182 + if num_groups == 1:
183 + groups = [
184 + KVCacheGroupSpec(
185 + ["model.layers.0.mla_attn", "model.layers.1.indexer"],
186 + spec,
187 + )
188 + ]
189 + else:
190 + groups = [
191 + KVCacheGroupSpec(["model.layers.0.mla_attn"], spec),
192 + KVCacheGroupSpec(["model.layers.1.indexer"], spec),
193 + ]
194 + return KVCacheConfig(
195 + num_blocks=num_blocks,
196 + kv_cache_tensors=[],
197 + kv_cache_groups=groups,
198 + )
199 +
200 +
201 class FakeMooncakeWrapper:
202 """Mock Mooncake TransferEngine for unit testing environments."""
203
@@ -235,6 +269,37 @@ def test_align_transfer_regions_uses_layer_name_occurrences():
269 assert [r.base_addr for r in aligned_local] == [0x1000, 0x1100]
270 assert [r.base_addr for r in aligned_remote] == [0xB000, 0xB100]
271
272 + # Hetero PP with no shared layers is an empty success. Same PP fails closed.
273 + p_regions = [
274 + _region(
275 + 0x1000,
276 + block_len=2048,
277 + kv_block_len=1024,
278 + row_offset=0,
279 + layer_name="model.layers.2.mla_attn",
280 + layer_index=2,
281 + )
282 + ]
283 + early_d = [
284 + _region(
285 + 0xA000,
286 + block_len=2048,
287 + kv_block_len=1024,
288 + row_offset=0,
289 + layer_name="model.layers.0.mla_attn",
290 + layer_index=0,
291 + )
292 + ]
293 + none_p, none_d, none_err = _align_transfer_regions(
294 + p_regions, early_d, allow_partial_layers=True
295 + )
296 + assert none_err is None
297 + assert none_p == [] and none_d == []
298 + _, _, mismatch_err = _align_transfer_regions(
299 + p_regions, early_d, allow_partial_layers=False
300 + )
301 + assert mismatch_err is not None
302 +
303
304 @pytest.mark.asyncio
305 async def test_build_transfer_params_separates_prefill_pp_layers():
@@ -323,18 +388,7 @@ async def test_build_transfer_params_separates_prefill_pp_layers():
388 local_block_ids=[[10, 11]],
389 ready=asyncio.Event(),
390 )
326 - xfer_meta = MooncakeXferMetadata(
327 - remote_hostname="consumer-host",
328 - remote_port=54321,
329 - remote_tp_size=1,
330 - remote_tp_rank=0,
331 - req_blocks={"d-req-pp": (transfer_id, [[20, 21]])},
332 - kv_caches_base_addr=[region.base_addr for region in remote_regions],
333 - block_lens=[region.block_len for region in remote_regions],
334 - kv_block_lens=[region.kv_block_len for region in remote_regions],
335 - registered_layer_names=[region.layer_name for region in remote_regions],
336 - registered_layer_indices=[region.layer_index for region in remote_regions],
337 - )
391 + xfer_meta = _xfer_meta(remote_regions, {"d-req-pp": (transfer_id, [[20, 21]])})
392
393 for pp_rank, local_regions in producer_pp_regions.items():
394 aligned_local, aligned_remote, err = _align_transfer_regions(
@@ -411,20 +465,24 @@ async def test_send_kv_to_decode_aligns_consumer_regions_by_layer_metadata(
465 prefill_worker.reqs_need_send[transfer_id] = send_meta
466 send_meta.ready.set()
467
414 - xfer_meta = MooncakeXferMetadata(
415 - remote_hostname="consumer-host",
416 - remote_port=54321,
417 - remote_tp_size=1,
418 - remote_tp_rank=0,
419 - req_blocks={"d-req-layer-align": (transfer_id, [[20]])},
420 - kv_caches_base_addr=[0xA000, 0xB000],
421 - block_lens=[block_len, block_len],
422 - kv_block_lens=[block_len, block_len],
423 - registered_layer_names=[
424 - "model.layers.0.self_attn",
425 - "model.layers.1.self_attn",
468 + xfer_meta = _xfer_meta(
469 + [
470 + _region(
471 + 0xA000,
472 + block_len=block_len,
473 + row_offset=-1,
474 + layer_name="model.layers.0.self_attn",
475 + layer_index=0,
476 + ),
477 + _region(
478 + 0xB000,
479 + block_len=block_len,
480 + row_offset=-1,
481 + layer_name="model.layers.1.self_attn",
482 + layer_index=1,
483 + ),
484 ],
427 - registered_layer_indices=[0, 1],
485 + {"d-req-layer-align": (transfer_id, [[20]])},
486 )
487 mock_socket = AsyncMock(spec=zmq.asyncio.Socket)
488 mock_socket.send_multipart = AsyncMock()
@@ -1093,6 +1151,81 @@ def patch_worker_dependencies():
1151 }
1152
1153
1154 +def _region(
1155 + base: int,
1156 + *,
1157 + block_len: int = 256,
1158 + kv_block_len: int | None = None,
1159 + row_offset: int | None = None,
1160 + layer_name: str = "layer",
1161 + layer_index: int = 0,
1162 + group_index: int = 0,
1163 + shared_group_ids: tuple[int, ...] = (),
1164 +) -> TransferRegion:
1165 + """One transfer slice. `row_offset` defaults to `base` for in-row addresses."""
1166 + return TransferRegion(
1167 + layer_name=layer_name,
1168 + layer_index=layer_index,
1169 + base_addr=base,
1170 + block_len=block_len,
1171 + kv_block_len=block_len if kv_block_len is None else kv_block_len,
1172 + group_index=group_index,
1173 + shared_group_ids=shared_group_ids,
1174 + row_offset=base if row_offset is None else row_offset,
1175 + )
1176 +
1177 +
1178 +def _xfer_meta(
1179 + regions: list[TransferRegion],
1180 + req_blocks: dict,
1181 + **overrides,
1182 +) -> MooncakeXferMetadata:
1183 + """Handshake metadata derived from the regions under test."""
1184 + fields = dict(
1185 + remote_hostname="consumer-host",
1186 + remote_port=54321,
1187 + remote_tp_size=1,
1188 + remote_tp_rank=0,
1189 + req_blocks=req_blocks,
1190 + kv_caches_base_addr=[region.base_addr for region in regions],
1191 + block_lens=[region.block_len for region in regions],
1192 + kv_block_lens=[region.kv_block_len for region in regions],
1193 + registered_layer_names=[region.layer_name for region in regions],
1194 + registered_layer_indices=[region.layer_index for region in regions],
1195 + registered_group_indices=[region.group_index for region in regions],
1196 + registered_shared_group_ids=[
1197 + list(region.shared_group_ids) for region in regions
1198 + ],
1199 + registered_row_offsets=[region.row_offset for region in regions],
1200 + )
1201 + fields.update(overrides)
1202 + return MooncakeXferMetadata(**fields)
1203 +
1204 +
1205 +@contextlib.contextmanager
1206 +def mooncake_register_worker(kv_cache_config, *, kv_role: str = "kv_consumer"):
1207 + """Build a worker whose engine registration and sender thread are patched."""
1208 + vllm_config = create_vllm_config(kv_connector="MooncakeConnector", kv_role=kv_role)
1209 + connector_mod = (
1210 + "vllm.distributed.kv_transfer.kv_connector.v1.mooncake.mooncake_connector"
1211 + )
1212 + with (
1213 + set_current_vllm_config(vllm_config),
1214 + patch_worker_dependencies(),
1215 + patch(f"{connector_mod}.threading.Event"),
1216 + patch(f"{connector_mod}.threading.Thread") as mock_thread,
1217 + ):
1218 + connector = MooncakeConnector(
1219 + vllm_config, KVConnectorRole.WORKER, kv_cache_config
1220 + )
1221 + worker = connector.connector_worker
1222 + mock_thread.return_value.is_alive.return_value = False
1223 + with patch.object(
1224 + worker.engine, "batch_register_memory", return_value=0
1225 + ) as reg:
1226 + yield worker, reg
1227 +
1228 +
1229 @pytest.mark.asyncio
1230 @pytest.mark.parametrize(
1231 ("local_pp_size", "local_pp_rank", "expected_addrs"),
@@ -1211,18 +1344,19 @@ async def test_heterogeneous_pp_waits_for_consumer_without_shared_layers():
1344 patch.object(worker, "_send_blocks", return_value=0) as send,
1345 ):
1346 for pp_rank in range(2):
1214 - metadata = MooncakeXferMetadata(
1347 + metadata = _xfer_meta(
1348 + [
1349 + _region(
1350 + 0x2000,
1351 + row_offset=-1,
1352 + layer_name=f"model.layers.{pp_rank}.self_attn",
1353 + layer_index=pp_rank,
1354 + )
1355 + ],
1356 + {"d-req": ("transfer", [[2]])},
1357 remote_hostname="consumer",
1358 remote_port=1234 + pp_rank,
1217 - remote_tp_size=1,
1218 - remote_tp_rank=0,
1359 remote_pp_size=2,
1220 - req_blocks={"d-req": ("transfer", [[2]])},
1221 - kv_caches_base_addr=[0x2000],
1222 - block_lens=[256],
1223 - kv_block_lens=[256],
1224 - registered_layer_names=[f"model.layers.{pp_rank}.self_attn"],
1225 - registered_layer_indices=[pp_rank],
1360 )
1361 await worker.send_kv_to_decode(b"consumer", sock, metadata)
1362 response = worker._xfer_resp_decoder.decode(
@@ -1286,17 +1420,17 @@ async def test_kv_producer(monkeypatch):
1420 send_meta.ready.set()
1421
1422 # Remote consumer request metadata
1289 - xfer_meta = MooncakeXferMetadata(
1290 - remote_hostname="consumer-host",
1291 - remote_port=54321,
1292 - remote_tp_size=1,
1293 - remote_tp_rank=0,
1294 - req_blocks={"d-req-1": (transfer_id, [[20, 21]])},
1295 - kv_caches_base_addr=[0x2000],
1296 - block_lens=[block_len],
1297 - kv_block_lens=[block_len],
1298 - registered_layer_names=["model.layers.0.self_attn"],
1299 - registered_layer_indices=[0],
1423 + xfer_meta = _xfer_meta(
1424 + [
1425 + _region(
1426 + 0x2000,
1427 + block_len=block_len,
1428 + row_offset=-1,
1429 + layer_name="model.layers.0.self_attn",
1430 + layer_index=0,
1431 + )
1432 + ],
1433 + {"d-req-1": (transfer_id, [[20, 21]])},
1434 )
1435
1436 mock_socket = AsyncMock(spec=zmq.asyncio.Socket)
@@ -1544,139 +1678,703 @@ async def test_worker_get_finished_timeout(monkeypatch):
1678 )
1679 def test_register_kv_caches(layout: KVCacheLayout, separate_kv_head_groups: bool):
1680 """Tests the memory registration logic with the underlying Mooncake engine."""
1547 - vllm_config = create_vllm_config(
1548 - kv_connector="MooncakeConnector", kv_role="kv_consumer"
1681 + spec = FullAttentionSpec(
1682 + block_size=16,
1683 + num_kv_heads=4,
1684 + head_size=64,
1685 + dtype=torch.float16,
1686 + num_head_slots=2 if separate_kv_head_groups else None,
1687 + state_content_bytes=4 * 64 * 2 if separate_kv_head_groups else None,
1688 )
1550 -
1551 - with (
1552 - set_current_vllm_config(vllm_config),
1553 - patch_worker_dependencies(),
1554 - patch(
1555 - "vllm.distributed.kv_transfer.kv_connector.v1.mooncake.mooncake_connector.threading.Event"
1556 - ),
1557 - patch(
1558 - "vllm.distributed.kv_transfer.kv_connector.v1.mooncake.mooncake_connector.threading.Thread"
1559 - ) as mock_thread,
1560 - ):
1561 - connector = MooncakeConnector(
1562 - vllm_config,
1563 - KVConnectorRole.WORKER,
1564 - _make_test_kv_cache_config(),
1565 - )
1566 - worker = connector.connector_worker
1567 - mock_thread.return_value.is_alive.return_value = False
1568 -
1569 - spec = FullAttentionSpec(
1570 - block_size=16,
1571 - num_kv_heads=4,
1572 - head_size=64,
1573 - dtype=torch.float16,
1574 - num_head_slots=2 if separate_kv_head_groups else None,
1575 - state_content_bytes=4 * 64 * 2 if separate_kv_head_groups else None,
1576 - )
1577 - layer_names = [
1578 - "model.layers.0.self_attn",
1579 - "model.layers.1.self_attn",
1580 - ]
1689 + layer_names = [
1690 + "model.layers.0.self_attn",
1691 + "model.layers.1.self_attn",
1692 + ]
1693 + with mooncake_register_worker(_make_test_kv_cache_config()) as (worker, reg):
1694 for layer_name in layer_names:
1695 worker._layer_specs[layer_name] = spec
1696 raw = torch.zeros(2 * 2 * spec.page_size_bytes, dtype=torch.int8)
1697 tensor1, tensor2 = dense_kv_cache_views(raw, spec, 2, 2, layout)
1698 kv_caches = dict(zip(layer_names, (tensor1, tensor2)))
1699 + worker.register_kv_caches(kv_caches)
1700 +
1701 + reg.assert_called_once()
1702 + registered_ptrs, registered_lens = reg.call_args[0]
1703 + assert registered_ptrs == [raw.data_ptr()]
1704 + assert registered_lens == [raw.nbytes]
1705 +
1706 + n_blocks = tensor1.shape[0] if tensor1.ndim > 1 else 1
1707 + storage_end = raw.data_ptr() + raw.nbytes
1708 + for base, stride, kv_len in zip(
1709 + worker.kv_caches_base_addr,
1710 + worker.block_len_per_layer,
1711 + worker.kv_block_len_per_layer,
1712 + ):
1713 + assert base + (n_blocks - 1) * stride + kv_len <= storage_end
1714 +
1715 + # Expected shape is the layout, not a recomputation of the pack predicate.
1716 + kind = {
1717 + KVCacheLayout.LBHNC: "page",
1718 + KVCacheLayout.BLHNC: "page",
1719 + KVCacheLayout.BHLNC: "opaque",
1720 + KVCacheLayout.LHBNC: "per_head",
1721 + }[layout]
1722 + if kind == "opaque":
1723 + packed_row = raw.nbytes // n_blocks
1724 + assert worker.kv_caches_base_addr == [raw.data_ptr()]
1725 + assert worker.block_len_per_layer == [packed_row]
1726 + assert worker.kv_block_len_per_layer == [packed_row]
1727 + assert worker.registered_layer_names == [layer_names[0]]
1728 + elif kind == "per_head":
1729 + expected_addrs = [
1730 + cache[:, head_idx].data_ptr()
1731 + for cache in (tensor1, tensor2)
1732 + for head_idx in range(cache.shape[1])
1733 + ]
1734 + head_block_bytes = tensor1.stride(0) * tensor1.element_size()
1735 + assert worker.kv_caches_base_addr == expected_addrs
1736 + assert worker.block_len_per_layer == [head_block_bytes] * len(
1737 + expected_addrs
1738 + )
1739 + assert worker.kv_block_len_per_layer == [head_block_bytes] * len(
1740 + expected_addrs
1741 + )
1742 + assert worker.registered_layer_names == [
1743 + layer_name
1744 + for layer_name in layer_names
1745 + for _ in range(tensor1.shape[1])
1746 + ]
1747 + else:
1748 + page_stride = tensor1.stride(0) * tensor1.element_size()
1749 + assert len(worker.block_len_per_layer) == len(kv_caches)
1750 + assert worker.block_len_per_layer == [page_stride, page_stride]
1751 + assert worker.kv_block_len_per_layer == [spec.page_size_bytes] * 2
1752 + assert worker.registered_layer_names == list(kv_caches)
1753 + assert worker.registered_layer_indices == [0, 1]
1754
1587 - with patch.object(
1588 - worker.engine, "batch_register_memory", return_value=0
1589 - ) as mock_batch_register:
1590 - connector.register_kv_caches(kv_caches)
1591 -
1592 - mock_batch_register.assert_called_once()
1593 - registered_ptrs, registered_lens = mock_batch_register.call_args[0]
1594 - assert registered_ptrs == [raw.data_ptr()]
1595 - assert registered_lens == [raw.nbytes]
1596 -
1597 - if not layout.is_block_compact:
1598 - expected_addrs = [
1599 - cache[:, head_idx].data_ptr()
1600 - for cache in (tensor1, tensor2)
1601 - for head_idx in range(cache.shape[1])
1602 - ]
1603 - head_block_bytes = tensor1.stride(0) * tensor1.element_size()
1604 - assert worker.kv_caches_base_addr == expected_addrs
1605 - assert worker.block_len_per_layer == [head_block_bytes] * len(
1606 - expected_addrs
1607 - )
1608 - assert worker.kv_block_len_per_layer == [head_block_bytes] * len(
1609 - expected_addrs
1610 - )
1611 - assert worker.registered_layer_names == [
1612 - layer_name
1613 - for layer_name in layer_names
1614 - for _ in range(tensor1.shape[1])
1615 - ]
1616 - else:
1617 - assert len(worker.block_len_per_layer) == len(kv_caches)
1618 - for bl in worker.block_len_per_layer:
1619 - assert bl == tensor1.stride(0) * tensor1.element_size()
1620 - assert worker.kv_block_len_per_layer == [spec.page_size_bytes] * 2
1621 - assert worker.registered_layer_names == list(kv_caches)
1622 - assert worker.registered_layer_indices == [0, 1]
1755
1756 +def test_register_noncontiguous_packed_allows_matching_pp():
1757 + """Same PP on both sides can still register a non-contiguous packed row.
1758
1625 -def test_register_kv_caches_supports_mixed_mla_and_eagle_shapes():
1626 - """Mixed MLA+Eagle caches should register by byte length, not shape."""
1627 - vllm_config = create_vllm_config(
1628 - kv_connector="MooncakeConnector", kv_role="kv_consumer"
1759 + A remote PP mismatch is rejected at send time, once the peer PP size is
1760 + known. Rejecting every local PP>1 layout at startup is too strict.
1761 + """
1762 + spec = FullAttentionSpec(
1763 + block_size=16, num_kv_heads=4, head_size=64, dtype=torch.float16
1764 )
1765 + layer_names = ["model.layers.0.self_attn", "model.layers.1.self_attn"]
1766 + with mooncake_register_worker(_make_test_kv_cache_config()) as (worker, _reg):
1767 + worker.pp_size = 2
1768 + for layer_name in layer_names:
1769 + worker._layer_specs[layer_name] = spec
1770 + raw = torch.zeros(2 * 2 * spec.page_size_bytes, dtype=torch.int8)
1771 + tensor1, tensor2 = dense_kv_cache_views(raw, spec, 2, 2, KVCacheLayout.BHLNC)
1772 + assert not tensor1[0].is_contiguous()
1773 + worker.register_kv_caches(dict(zip(layer_names, (tensor1, tensor2))))
1774 + assert worker.kv_caches_base_addr == [raw.data_ptr()]
1775 + assert worker.region_row_offsets == [0]
1776
1631 - with (
1632 - set_current_vllm_config(vllm_config),
1633 - patch_worker_dependencies(),
1634 - patch(
1635 - "vllm.distributed.kv_transfer.kv_connector.v1.mooncake.mooncake_connector.threading.Event"
1636 - ),
1637 - patch(
1638 - "vllm.distributed.kv_transfer.kv_connector.v1.mooncake.mooncake_connector.threading.Thread"
1639 - ) as mock_thread,
1640 - ):
1641 - connector = MooncakeConnector(
1642 - vllm_config,
1643 - KVConnectorRole.WORKER,
1644 - _make_test_kv_cache_config(),
1645 - )
1646 - worker = connector.connector_worker
1647 - mock_thread.return_value.is_alive.return_value = False
1777
1778 +def test_register_kv_caches_supports_mixed_mla_and_eagle_shapes():
1779 + """Mixed MLA+Eagle caches should register by byte length, not shape."""
1780 + with mooncake_register_worker(_make_test_kv_cache_config()) as (worker, reg):
1781 worker.use_mla = True
1782 worker.transfer_topo.is_mla = True
1651 -
1652 - # MLA cache tensor: shape[-2] is the block size and each block's
1653 - # byte stride matches the cache spec page size.
1783 mla_cache = torch.zeros((2, 16, 512), dtype=torch.float16)
1655 - # Eagle3/GQA-like cache tensor: shape[-2] is num_kv_heads, not block size.
1784 eagle_cache = torch.zeros((2, 16, 8, 64), dtype=torch.float16)
1785 kv_caches = {
1786 "model.layers.0.mla_attn": mla_cache,
1787 "model.layers.1.eagle_attn": eagle_cache,
1788 }
1789 + worker.register_kv_caches(kv_caches)
1790 +
1791 + reg.assert_called_once()
1792 + registered_ptrs, registered_lens = reg.call_args[0]
1793 + assert registered_ptrs == [mla_cache.data_ptr(), eagle_cache.data_ptr()]
1794 + assert registered_lens == [mla_cache.nbytes, eagle_cache.nbytes]
1795 + assert worker.block_len_per_layer == [
1796 + mla_cache.nbytes // mla_cache.shape[0],
1797 + eagle_cache.nbytes // eagle_cache.shape[0],
1798 + ]
1799 + assert worker.registered_layer_names == [
1800 + "model.layers.0.mla_attn",
1801 + "model.layers.1.eagle_attn",
1802 + ]
1803 + assert worker.registered_layer_indices == [0, 1]
1804 + assert len(worker.kv_caches_base_addr) == 2
1805 +
1806 +
1807 +def test_packed_and_unpacked_region_lengths_fail_homogeneous_tp_handshake():
1808 + packed = TransferRegion(
1809 + layer_name="model.layers.0.mla_attn",
1810 + layer_index=0,
1811 + base_addr=0x1000,
1812 + block_len=2048,
1813 + kv_block_len=2048,
1814 + group_index=0,
1815 + )
1816 + unpacked = TransferRegion(
1817 + layer_name="model.layers.0.mla_attn",
1818 + layer_index=0,
1819 + base_addr=0x1000,
1820 + block_len=64,
1821 + kv_block_len=64,
1822 + group_index=0,
1823 + )
1824 + err = _validate_asymmetric_region_lengths(
1825 + local_regions=[packed],
1826 + remote_regions=[unpacked],
1827 + local_tp_size=1,
1828 + remote_tp_size=1,
1829 + producer_cache_replicated=False,
1830 + )
1831 + assert err is not None
1832 + assert "length mismatch" in err
1833
1662 - with patch.object(
1663 - worker.engine, "batch_register_memory", return_value=0
1664 - ) as mock_batch_register:
1665 - connector.register_kv_caches(kv_caches)
1666 -
1667 - mock_batch_register.assert_called_once()
1668 - registered_ptrs, registered_lens = mock_batch_register.call_args[0]
1669 - assert registered_ptrs == [mla_cache.data_ptr(), eagle_cache.data_ptr()]
1670 - assert registered_lens == [mla_cache.nbytes, eagle_cache.nbytes]
1671 - assert worker.block_len_per_layer == [
1672 - mla_cache.nbytes // mla_cache.shape[0],
1673 - eagle_cache.nbytes // eagle_cache.shape[0],
1674 - ]
1675 - assert worker.registered_layer_names == [
1676 - "model.layers.0.mla_attn",
1677 - "model.layers.1.eagle_attn",
1834 + count_err = _validate_asymmetric_region_lengths(
1835 + local_regions=[packed],
1836 + remote_regions=[unpacked, unpacked],
1837 + local_tp_size=1,
1838 + remote_tp_size=1,
1839 + producer_cache_replicated=False,
1840 + )
1841 + assert count_err is not None
1842 + assert "region counts" in count_err
1843 +
1844 +
1845 +@pytest.mark.parametrize(
1846 + ("num_groups", "expected_group_index"),
1847 + [(1, 0), (2, _SHARED_REGION_GROUP_ID)],
1848 +)
1849 +def test_register_kv_caches_collapses_shared_mla_storage(
1850 + num_groups, expected_group_index
1851 +):
1852 + """Shared backing collapses to one packed region; two groups mark it -1."""
1853 + num_blocks = 4
1854 + packed_row = 256
1855 + with mooncake_register_worker(
1856 + _make_packed_mla_kv_cache_config(num_blocks, num_groups=num_groups)
1857 + ) as (worker, reg):
1858 + backing = torch.zeros((num_blocks, packed_row), dtype=torch.uint8)
1859 + worker.register_kv_caches(
1860 + {
1861 + "model.layers.0.mla_attn": backing,
1862 + "model.layers.1.indexer": backing,
1863 + }
1864 + )
1865 +
1866 + reg.assert_called_once()
1867 + registered_ptrs, registered_lens = reg.call_args[0]
1868 + assert registered_ptrs == [backing.data_ptr()]
1869 + assert registered_lens == [backing.untyped_storage().nbytes()]
1870 + assert worker.kv_caches_base_addr == [backing.data_ptr()]
1871 + assert worker.block_len_per_layer == [packed_row]
1872 + assert worker.kv_block_len_per_layer == [packed_row]
1873 + assert worker.registered_layer_names == ["model.layers.0.mla_attn"]
1874 + assert worker.registered_group_indices == [expected_group_index]
1875 + if expected_group_index == _SHARED_REGION_GROUP_ID:
1876 + assert worker.region_shared_groups == [(0, 1)]
1877 +
1878 +
1879 +def test_block_ids_for_region_flattens_shared_groups():
1880 + ids = [[10, 11], [12]]
1881 + assert _block_ids_for_region(ids, 0) == [10, 11]
1882 + assert _block_ids_for_region(ids, 1) == [12]
1883 + assert _block_ids_for_region(ids, _SHARED_REGION_GROUP_ID, (0, 1)) == [
1884 + 10,
1885 + 11,
1886 + 12,
1887 + ]
1888 + # A group that does not share the allocation stays out.
1889 + assert _block_ids_for_region(
1890 + [[10, 11], [12], [99]], _SHARED_REGION_GROUP_ID, (0, 1)
1891 + ) == [10, 11, 12]
1892 +
1893 +
1894 +@pytest.mark.asyncio
1895 +@pytest.mark.parametrize(
1896 + ("group_index", "local_ids", "remote_ids", "n_blocks"),
1897 + [
1898 + (0, [[10, 11]], [[20, 21]], 2),
1899 + (_SHARED_REGION_GROUP_ID, [[10, 11], [12]], [[20, 21], [22]], 3),
1900 + ],
1901 +)
1902 +async def test_build_transfer_params_sends_packed_region_once(
1903 + group_index, local_ids, remote_ids, n_blocks
1904 +):
1905 + """Packed region coalesces contiguous blocks, including shared-group flatten."""
1906 + worker = MooncakeConnectorWorker.__new__(MooncakeConnectorWorker)
1907 + worker.async_zmq_ctx = MagicMock()
1908 + worker.is_kv_consumer = True
1909 + worker.is_kv_producer = True
1910 + worker.tp_rank = 0
1911 + worker.tp_size = 1
1912 + worker.use_mla = True
1913 + worker.kv_cache_config = _make_packed_mla_kv_cache_config(
1914 + num_blocks=4, num_groups=len(local_ids)
1915 + )
1916 + worker._physical_blocks_per_logical_kv_block = 1
1917 + worker.transfer_topo = SimpleNamespace(
1918 + local_replicates_kv_cache=False,
1919 + total_num_kv_heads=1,
1920 + )
1921 +
1922 + block_len = 256
1923 + shared = (0, 1) if group_index == _SHARED_REGION_GROUP_ID else ()
1924 + local_region = _region(
1925 + 0x1000,
1926 + block_len=block_len,
1927 + row_offset=0,
1928 + layer_name="model.layers.0.mla_attn",
1929 + group_index=group_index,
1930 + shared_group_ids=shared,
1931 + )
1932 + remote_region = _region(
1933 + 0xA000,
1934 + block_len=block_len,
1935 + row_offset=0,
1936 + layer_name="model.layers.0.mla_attn",
1937 + group_index=group_index,
1938 + shared_group_ids=shared,
1939 + )
1940 + transfer_id = "xfer-packed"
1941 + send_meta = SendBlockMeta(
1942 + p_req_id="p-packed",
1943 + transfer_id=transfer_id,
1944 + local_block_ids=local_ids,
1945 + ready=asyncio.Event(),
1946 + )
1947 + xfer_meta = _xfer_meta([remote_region], {"d-packed": (transfer_id, remote_ids)})
1948 +
1949 + (
1950 + src_ptrs,
1951 + dst_ptrs,
1952 + lengths,
1953 + err_reqs,
1954 + err_msg,
1955 + ) = await worker._build_transfer_params(
1956 + ready_reqs=[("d-packed", send_meta)],
1957 + agent_meta=xfer_meta,
1958 + local_regions=[local_region],
1959 + remote_regions=[remote_region],
1960 + )
1961 +
1962 + assert err_reqs == []
1963 + assert err_msg is None
1964 + assert src_ptrs == [0x1000 + 10 * block_len]
1965 + assert dst_ptrs == [0xA000 + 20 * block_len]
1966 + assert lengths == [n_blocks * block_len]
1967 +
1968 +
1969 +def test_coalesce_promotes_padding_only_for_a_full_row():
1970 + """A full-row merge includes the padding tail; a partial row does not."""
1971 + page, row = 1024, 2560
1972 + local = [
1973 + _region(0, block_len=row, kv_block_len=page, row_offset=0),
1974 + _region(page, block_len=row, kv_block_len=page, row_offset=page),
1975 + ]
1976 + remote = [
1977 + _region(100, block_len=row, kv_block_len=page, row_offset=0),
1978 + _region(100 + page, block_len=row, kv_block_len=page, row_offset=page),
1979 + ]
1980 + full_local, full_remote = _coalesce_contiguous_transfer_regions(
1981 + local, remote, promote_full_row=True
1982 + )
1983 + assert len(full_local) == 1
1984 + assert full_local[0].kv_block_len == full_remote[0].kv_block_len == row
1985 +
1986 + partial_local, _ = _coalesce_contiguous_transfer_regions(local, remote)
1987 + assert partial_local[0].kv_block_len == 2 * page
1988 +
1989 +
1990 +def test_coalesce_spans_padding_between_pages():
1991 + """Unpadded pages merge across the padding gap without a full-row lift."""
1992 + row = 2560
1993 + merged, _ = _coalesce_contiguous_transfer_regions(
1994 + [
1995 + _region(0, block_len=row, kv_block_len=1000),
1996 + _region(1024, block_len=row, kv_block_len=1000),
1997 + ],
1998 + [
1999 + _region(5000, block_len=row, kv_block_len=1000, row_offset=0),
2000 + _region(6024, block_len=row, kv_block_len=1000, row_offset=1024),
2001 + ],
2002 + )
2003 + assert len(merged) == 1
2004 + assert merged[0].kv_block_len == 2024
2005 + assert merged[0].row_offset == 0
2006 +
2007 +
2008 +def test_coalesce_promotes_each_group_that_starts_at_row_zero():
2009 + """Groups overlay one row, so each row-start run is its own full-row copy."""
2010 + page, gap, row = 1000, 1024, 4096
2011 +
2012 + def side(origin: int) -> list[TransferRegion]:
2013 + return [
2014 + _region(
2015 + origin + base,
2016 + block_len=row,
2017 + kv_block_len=page,
2018 + row_offset=offset,
2019 + layer_name=f"group-{group}",
2020 + layer_index=group,
2021 + group_index=group,
2022 + )
2023 + for group, base, offset in (
2024 + (0, 0, 0),
2025 + (0, gap, gap),
2026 + (1, 0, 0),
2027 + (1, gap, gap),
2028 + )
2029 ]
1679 - assert worker.registered_layer_indices == [0, 1]
2030 +
2031 + merged_local, merged_remote = _coalesce_contiguous_transfer_regions(
2032 + side(0), side(8000), promote_full_row=True
2033 + )
2034 + assert [region.group_index for region in merged_local] == [0, 1]
2035 + assert [region.kv_block_len for region in merged_local] == [row, row]
2036 + assert [region.kv_block_len for region in merged_remote] == [row, row]
2037 + assert merged_local[1].base_addr == 0
2038 + assert merged_remote[1].base_addr == 8000
2039 +
2040 +
2041 +def test_coalesce_does_not_promote_a_mid_row_run():
2042 + """A matched PP slice that does not start at row offset 0 stays partial."""
2043 + page, row = 1000, 4096
2044 + merged, _ = _coalesce_contiguous_transfer_regions(
2045 + [
2046 + _region(
2047 + 1024, block_len=row, kv_block_len=page, row_offset=1024, layer_index=2
2048 + ),
2049 + _region(
2050 + 2048, block_len=row, kv_block_len=page, row_offset=2048, layer_index=2
2051 + ),
2052 + ],
2053 + [
2054 + _region(
2055 + 9000 + 1024,
2056 + block_len=row,
2057 + kv_block_len=page,
2058 + row_offset=1024,
2059 + layer_index=2,
2060 + ),
2061 + _region(
2062 + 9000 + 2048,
2063 + block_len=row,
2064 + kv_block_len=page,
2065 + row_offset=2048,
2066 + layer_index=2,
2067 + ),
2068 + ],
2069 + promote_full_row=True,
2070 + )
2071 + assert len(merged) == 1
2072 + assert merged[0].row_offset == 1024
2073 + assert merged[0].kv_block_len == 1024 + page
2074 +
2075 +
2076 +def test_opaque_packed_row_is_full_stride_at_offset_zero():
2077 + """Branch B is row-sized at offset 0. A page view at offset 0 is not."""
2078 + assert _has_opaque_packed_row([0], [4096], [4096])
2079 + assert not _has_opaque_packed_row([0, 1024], [1000, 1000], [4096, 4096])
2080 + assert not _has_opaque_packed_row([], [4096], [4096])
2081 +
2082 +
2083 +def test_pp_mismatch_rejects_opaque_row_in_both_directions():
2084 + """The smaller PP side can be either the producer or the consumer."""
2085 + page_view = dict(
2086 + remote_row_offsets=[0, 1024],
2087 + remote_kv_block_lens=[1000, 1000],
2088 + remote_block_lens=[4096, 4096],
2089 + )
2090 + opaque_row = dict(
2091 + remote_row_offsets=[0],
2092 + remote_kv_block_lens=[4096],
2093 + remote_block_lens=[4096],
2094 + )
2095 + # P pp=2, D pp=1, local row is opaque.
2096 + assert _pp_mismatch_hides_packed_layers(
2097 + 2, 1, local_has_opaque_row=True, **page_view
2098 + )
2099 + # P pp=1, D pp=2, remote row is opaque.
2100 + assert _pp_mismatch_hides_packed_layers(
2101 + 1, 2, local_has_opaque_row=False, **opaque_row
2102 + )
2103 + # Matching PP still aligns by the shared first layer name.
2104 + assert not _pp_mismatch_hides_packed_layers(
2105 + 2, 2, local_has_opaque_row=True, **opaque_row
2106 + )
2107 + # Hetero PP with page-contiguous views is not this rejection.
2108 + assert not _pp_mismatch_hides_packed_layers(
2109 + 2, 1, local_has_opaque_row=False, **page_view
2110 + )
2111 +
2112 +
2113 +def test_prepare_transfer_regions_reuses_success_and_error():
2114 + """A peer layout is built once. Re-registering drops the cache."""
2115 + worker, _, _, _ = _register_sliced_packed_mla([0, 1])
2116 + regions = worker._get_transfer_regions(
2117 + worker.kv_caches_base_addr,
2118 + worker.block_len_per_layer,
2119 + worker.kv_block_len_per_layer,
2120 + worker.registered_layer_names,
2121 + worker.registered_layer_indices,
2122 + worker.registered_group_indices,
2123 + [tuple(groups) for groups in worker.region_shared_groups],
2124 + worker.region_row_offsets,
2125 + )
2126 + meta = _xfer_meta(
2127 + regions,
2128 + {},
2129 + remote_hostname="peer",
2130 + remote_port=1,
2131 + remote_tp_size=worker.tp_size,
2132 + remote_pp_size=worker.pp_size,
2133 + )
2134 + calls = 0
2135 + original = worker._get_transfer_regions
2136 +
2137 + def counting(*args, **kwargs):
2138 + nonlocal calls
2139 + calls += 1
2140 + return original(*args, **kwargs)
2141 +
2142 + worker._get_transfer_regions = counting
2143 + prepared = worker._prepare_transfer_regions(meta)
2144 + assert prepared[2] is None
2145 + assert calls == 2
2146 + assert worker._prepare_transfer_regions(meta) == prepared
2147 + assert calls == 2
2148 +
2149 + mismatch = _xfer_meta(
2150 + [
2151 + _region(
2152 + 0x2000,
2153 + block_len=1024,
2154 + row_offset=0,
2155 + layer_name="model.layers.9.mla_attn",
2156 + layer_index=9,
2157 + )
2158 + ],
2159 + {},
2160 + remote_hostname="peer",
2161 + remote_port=2,
2162 + remote_tp_size=worker.tp_size,
2163 + remote_pp_size=worker.pp_size,
2164 + )
2165 + failed = worker._prepare_transfer_regions(mismatch)
2166 + assert failed[2] is not None
2167 + assert calls == 4
2168 + assert worker._prepare_transfer_regions(mismatch)[2] == failed[2]
2169 + assert calls == 4
2170 +
2171 + page = 1024
2172 + backing = torch.zeros((4, page * 2), dtype=torch.uint8)
2173 + kv_caches = {
2174 + "model.layers.0.mla_attn": backing[:, :page],
2175 + "model.layers.1.mla_attn": backing[:, page:],
2176 + }
2177 + worker.is_kv_consumer = True
2178 + # The first register built a producer. Its thread patch is already gone,
2179 + # so stay on the consumer path and return before the sender thread starts.
2180 + with patch.object(worker.engine, "batch_register_memory", return_value=0):
2181 + worker.register_kv_caches(kv_caches)
2182 + assert worker._prepared_transfer_regions == {}
2183 + worker._prepare_transfer_regions(meta)
2184 + assert calls == 6
2185 +
2186 +
2187 +def test_coalesce_keeps_a_run_inside_its_starting_row():
2188 + """The row bound is the run start, not the previous slice."""
2189 + row = 2500
2190 + slices = [
2191 + _region(base, block_len=row, kv_block_len=1000) for base in (0, 1000, 2000)
2192 + ]
2193 + merged, _ = _coalesce_contiguous_transfer_regions(slices, slices)
2194 + assert len(merged) == 2
2195 + assert merged[0].kv_block_len == 2000
2196 + assert merged[1].base_addr == 2000
2197 + assert merged[1].kv_block_len == 1000
2198 +
2199 +
2200 +def _layer_name(idx: int) -> str:
2201 + return f"model.layers.{idx}.mla_attn"
2202 +
2203 +
2204 +def _register_sliced_packed_mla(
2205 + layer_idxs: list[int], *, page: int = 1024, num_blocks: int = 4
2206 +):
2207 + """Register column slices of one MLA packed row. Returns worker fields."""
2208 + spec = MLAAttentionSpec(
2209 + block_size=16,
2210 + num_kv_heads=1,
2211 + head_size=64,
2212 + dtype=torch.uint8,
2213 + )
2214 + kv_cache_config = KVCacheConfig(
2215 + num_blocks=num_blocks,
2216 + kv_cache_tensors=[],
2217 + kv_cache_groups=[
2218 + KVCacheGroupSpec([_layer_name(i) for i in layer_idxs], spec),
2219 + ],
2220 + )
2221 + backing = torch.zeros((num_blocks, page * len(layer_idxs)), dtype=torch.uint8)
2222 + kv_caches = {
2223 + _layer_name(idx): backing[:, i * page : (i + 1) * page]
2224 + for i, idx in enumerate(layer_idxs)
2225 + }
2226 + with mooncake_register_worker(kv_cache_config, kv_role="kv_producer") as (
2227 + worker,
2228 + _,
2229 + ):
2230 + worker.register_kv_caches(kv_caches)
2231 + regions = worker._get_transfer_regions(
2232 + worker.kv_caches_base_addr,
2233 + worker.block_len_per_layer,
2234 + worker.kv_block_len_per_layer,
2235 + worker.registered_layer_names,
2236 + worker.registered_layer_indices,
2237 + worker.registered_group_indices,
2238 + row_offsets=worker.region_row_offsets,
2239 + )
2240 + return worker, kv_cache_config, regions, backing.data_ptr()
2241 +
2242 +
2243 +def test_sliced_packed_mla_registers_each_layer_view():
2244 + """Each column of a packed MLA row stays its own view, in row order."""
2245 + page = 1024
2246 + layer_idxs = [0, 1]
2247 + with mooncake_register_worker(
2248 + KVCacheConfig(
2249 + num_blocks=4,
2250 + kv_cache_tensors=[],
2251 + kv_cache_groups=[
2252 + KVCacheGroupSpec(
2253 + [_layer_name(i) for i in layer_idxs],
2254 + MLAAttentionSpec(
2255 + block_size=16,
2256 + num_kv_heads=1,
2257 + head_size=64,
2258 + dtype=torch.uint8,
2259 + ),
2260 + )
2261 + ],
2262 + ),
2263 + kv_role="kv_producer",
2264 + ) as (worker, reg):
2265 + backing = torch.zeros((4, page * len(layer_idxs)), dtype=torch.uint8)
2266 + worker.register_kv_caches(
2267 + {
2268 + _layer_name(idx): backing[:, i * page : (i + 1) * page]
2269 + for i, idx in enumerate(layer_idxs)
2270 + }
2271 + )
2272 + reg.assert_called_once()
2273 + assert reg.call_args[0][0] == [backing.data_ptr()]
2274 + assert worker.region_row_offsets == [0, page]
2275 +
2276 +
2277 +async def _copy_packed_slices(
2278 + p_layers: list[int],
2279 + d_layers: list[int],
2280 + *,
2281 + allow_partial: bool,
2282 + local_ids: list[list[int]],
2283 + remote_ids: list[list[int]],
2284 + page: int = 1024,
2285 +):
2286 + p_worker, p_config, p_regions, p_base = _register_sliced_packed_mla(
2287 + p_layers, page=page
2288 + )
2289 + _, _, d_regions, d_base = _register_sliced_packed_mla(d_layers, page=page)
2290 + aligned_p, aligned_d, err = _align_transfer_regions(
2291 + p_regions, d_regions, allow_partial_layers=allow_partial
2292 + )
2293 + assert err is None
2294 + coalesced_p, coalesced_d = _coalesce_contiguous_transfer_regions(
2295 + aligned_p, aligned_d
2296 + )
2297 + transfer_id = "xfer-packed-slices"
2298 + send_meta = SendBlockMeta(
2299 + p_req_id="p-slices",
2300 + transfer_id=transfer_id,
2301 + local_block_ids=local_ids,
2302 + ready=asyncio.Event(),
2303 + )
2304 + p_worker.kv_cache_config = p_config
2305 + src, dst, lengths, err_reqs, err_msg = await p_worker._build_transfer_params(
2306 + ready_reqs=[("d-slices", send_meta)],
2307 + agent_meta=_xfer_meta(coalesced_d, {"d-slices": (transfer_id, remote_ids)}),
2308 + local_regions=coalesced_p,
2309 + remote_regions=coalesced_d,
2310 + )
2311 + return SimpleNamespace(
2312 + p_regions=p_regions,
2313 + d_regions=d_regions,
2314 + coalesced_p=coalesced_p,
2315 + coalesced_d=coalesced_d,
2316 + p_base=p_base,
2317 + d_base=d_base,
2318 + src=src,
2319 + dst=dst,
2320 + lengths=lengths,
2321 + err_reqs=err_reqs,
2322 + err_msg=err_msg,
2323 + )
2324 +
2325 +
2326 +@pytest.mark.asyncio
2327 +async def test_packed_hetero_pp_keeps_shared_layers():
2328 + """P-PP4/D-PP2 copies the shared MLA span, not either side's full row."""
2329 + page = 1024
2330 + copied = await _copy_packed_slices(
2331 + [2, 3],
2332 + [0, 1, 2, 3],
2333 + allow_partial=True,
2334 + local_ids=[[10, 11]],
2335 + remote_ids=[[10, 11]],
2336 + page=page,
2337 + )
2338 + assert [region.layer_index for region in copied.p_regions] == [2, 3]
2339 + assert [region.kv_block_len for region in copied.p_regions] == [page, page]
2340 + assert copied.p_regions[0].block_len == 2 * page
2341 + assert copied.d_regions[0].block_len == 4 * page
2342 + assert len(copied.coalesced_p) == 1
2343 + assert copied.coalesced_p[0].kv_block_len == 2 * page
2344 + assert copied.coalesced_d[0].base_addr == copied.d_base + 2 * page
2345 + assert copied.err_reqs == []
2346 + assert copied.err_msg is None
2347 + assert copied.lengths == [2 * page, 2 * page]
2348 + assert copied.src == [
2349 + copied.p_base + 10 * (2 * page),
2350 + copied.p_base + 11 * (2 * page),
2351 + ]
2352 + assert copied.dst == [
2353 + copied.d_base + 2 * page + 10 * (4 * page),
2354 + copied.d_base + 2 * page + 11 * (4 * page),
2355 + ]
2356 +
2357 +
2358 +@pytest.mark.asyncio
2359 +async def test_packed_matching_layers_coalesce_to_one_copy():
2360 + """Identical packed layer sets still emit one copy for contiguous blocks."""
2361 + page = 1024
2362 + copied = await _copy_packed_slices(
2363 + [0, 1],
2364 + [0, 1],
2365 + allow_partial=False,
2366 + local_ids=[[10, 11]],
2367 + remote_ids=[[20, 21]],
2368 + page=page,
2369 + )
2370 + assert copied.err_reqs == []
2371 + assert copied.err_msg is None
2372 + assert len(copied.coalesced_p) == 1
2373 + assert copied.coalesced_p[0].kv_block_len == copied.coalesced_p[0].block_len
2374 + block_len = 2 * page
2375 + assert copied.src == [copied.p_base + 10 * block_len]
2376 + assert copied.dst == [copied.d_base + 20 * block_len]
2377 + assert copied.lengths == [2 * block_len]
2378
2379
2380 @pytest.mark.asyncio
@@ -1760,22 +2458,24 @@ async def test_kv_producer_heterogeneous_tp(monkeypatch, d_tp_size):
2458 ) as mock_send_blocks:
2459 for d_rank in target_d_ranks:
2460 remote_block_ids = d_rank_remote_blocks[d_rank]
1763 - xfer_meta = MooncakeXferMetadata(
1764 - remote_hostname="consumer-host",
1765 - remote_port=54321,
1766 - remote_tp_size=d_tp_size,
1767 - remote_tp_rank=d_rank,
1768 - req_blocks={
2461 + xfer_meta = _xfer_meta(
2462 + [
2463 + _region(
2464 + 0x2000,
2465 + block_len=remote_block_len,
2466 + row_offset=-1,
2467 + layer_name="model.layers.0.self_attn",
2468 + layer_index=0,
2469 + )
2470 + ],
2471 + {
2472 f"d-req-h1-r{d_rank}": (
2473 transfer_id,
2474 remote_block_ids,
2475 )
2476 },
1774 - kv_caches_base_addr=[0x2000],
1775 - block_lens=[remote_block_len],
1776 - kv_block_lens=[remote_block_len],
1777 - registered_layer_names=["model.layers.0.self_attn"],
1778 - registered_layer_indices=[0],
2477 + remote_tp_size=d_tp_size,
2478 + remote_tp_rank=d_rank,
2479 )
2480
2481 mock_send_blocks.reset_mock()
tests/v1/kv_connector/unit/test_mooncake_connector_hybrid_mamba.py
+2
@@ -242,6 +242,8 @@ async def test_build_transfer_params_uses_non_overlapping_physical_pages():
242 kv_cache_config = make_hybrid_gdn_kv_cache_config(block_size=16)
243 worker = object.__new__(MooncakeConnectorWorker)
244 worker.shutdown = noop_shutdown
245 + # __new__ skips __init__, where this cache is created. register_kv_caches clears it.
246 + worker._prepared_transfer_regions = {}
247 worker.use_mla = False
248 worker.engine = SimpleNamespace(batch_register_memory=lambda *_: 0)
249 worker.is_kv_consumer = True
vllm/distributed/kv_transfer/kv_connector/v1/mooncake/mooncake_connector.py
+517 -42
@@ -1,6 +1,7 @@
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 import asyncio
4 +import itertools
5 import logging
6 import queue
7 import threading
@@ -62,6 +63,8 @@ from vllm.v1.kv_cache_interface import (
63 KpoolTailSpec,
64 KVCacheSpec,
65 MambaSpec,
66 + MLAAttentionSpec,
67 + SlidingWindowMLASpec,
68 SlidingWindowSpec,
69 )
70 from vllm.v1.request import RequestStatus
@@ -90,6 +93,12 @@ if TYPE_CHECKING:
93 ReqId = str # Internal scheduler request ID
94 TransferId = str # KV transfer coordination ID (shared by P/D)
95
96 +# Follow NIXL (`nixl/base_worker.py`): HMA packed pages can put more than one
97 +# KV group on a single allocation. group_index=-1 flattens every group's block
98 +# ids onto that region. Hybrid KV manager assigns distinct ids per group, so
99 +# flatten does not copy the same block twice.
100 +_SHARED_REGION_GROUP_ID = -1
101 +
102
103 @dataclass(frozen=True)
104 class TransferRegion:
@@ -99,6 +108,34 @@ class TransferRegion:
108 block_len: int
109 kv_block_len: int
110 group_index: int = 0
111 + # Groups that share this allocation. Empty unless group_index is -1.
112 + shared_group_ids: tuple[int, ...] = ()
113 + # Byte offset of this view from the start of its block row.
114 + row_offset: int = 0
115 +
116 +
117 +def _block_ids_for_region(
118 + block_ids_by_group: list[list[int]],
119 + group_index: int,
120 + shared_group_ids: tuple[int, ...] = (),
121 +) -> list[int]:
122 + """Follow NIXL `_block_ids_by_region`.
123 +
124 + -1 flattens only the groups that share this allocation. NIXL can flatten
125 + every group because its packed flag means they all share one storage;
126 + Mooncake can still have a separate Mamba group in the same request.
127 + """
128 + if group_index == _SHARED_REGION_GROUP_ID:
129 + assert shared_group_ids, "Shared packed region is missing its group ids."
130 + return list(
131 + itertools.chain.from_iterable(
132 + block_ids_by_group[group] for group in shared_group_ids
133 + )
134 + )
135 + assert 0 <= group_index < len(block_ids_by_group), (
136 + "Transfer region references a missing KV group."
137 + )
138 + return list(block_ids_by_group[group_index])
139
140
141 def _get_tp_ratio(local_tp_size: int, remote_tp_size: int) -> int:
@@ -129,6 +166,8 @@ def _expand_transfer_regions(
166 layer_names: list[str],
167 layer_indices: list[int],
168 group_indices: list[int] | None = None,
169 + shared_group_ids: list[tuple[int, ...]] | None = None,
170 + row_offsets: list[int] | None = None,
171 ) -> list[TransferRegion]:
172 """Expand registered KV tensors into the regions transferred by Mooncake."""
173 assert (
@@ -150,6 +189,14 @@ def _expand_transfer_regions(
189 "Mooncake transfer regions require matching group metadata lengths, "
190 f"got group_indices={len(group_indices)}, layer_names={len(layer_names)}."
191 )
192 + if shared_group_ids is None:
193 + shared_group_ids = [() for _ in layer_names]
194 + assert len(shared_group_ids) == len(layer_names)
195 + # Missing offsets must not look like row starts, or every run would be
196 + # promoted to the full row.
197 + if row_offsets is None:
198 + row_offsets = [-1] * len(layer_names)
199 + assert len(row_offsets) == len(layer_names)
200 regions: list[TransferRegion] = []
201 for (
202 base_addr,
@@ -158,6 +205,8 @@ def _expand_transfer_regions(
205 layer_name,
206 layer_index,
207 group_index,
208 + region_groups,
209 + row_offset,
210 ) in zip(
211 base_addrs,
212 block_lens,
@@ -165,6 +214,8 @@ def _expand_transfer_regions(
214 layer_names,
215 layer_indices,
216 group_indices,
217 + shared_group_ids,
218 + row_offsets,
219 ):
220 regions.append(
221 TransferRegion(
@@ -174,6 +225,8 @@ def _expand_transfer_regions(
225 block_len=block_len,
226 kv_block_len=kv_block_len,
227 group_index=group_index,
228 + shared_group_ids=region_groups,
229 + row_offset=row_offset,
230 )
231 )
232 return regions
@@ -377,12 +430,168 @@ def _align_transfer_regions(
430 f"{remote_region.group_index}."
431 ),
432 )
433 + if (
434 + local_region.shared_group_ids
435 + and remote_region.shared_group_ids
436 + and local_region.shared_group_ids != remote_region.shared_group_ids
437 + ):
438 + return (
439 + [],
440 + [],
441 + (
442 + "Mooncake shared-group set mismatch for "
443 + f"{local_region.layer_name}: producer="
444 + f"{local_region.shared_group_ids}, consumer="
445 + f"{remote_region.shared_group_ids}."
446 + ),
447 + )
448 aligned_local.append(local_region)
449 aligned_remote.append(remote_region)
450
451 return aligned_local, aligned_remote, None
452
453
454 +def _coalesce_contiguous_transfer_regions(
455 + local_regions: list[TransferRegion],
456 + remote_regions: list[TransferRegion],
457 + *,
458 + promote_full_row: bool = False,
459 +) -> tuple[list[TransferRegion], list[TransferRegion]]:
460 + """Merge aligned packed slices that are adjacent on both sides.
461 +
462 + Page-contiguous views stay addressable by layer name for hetero PP
463 + (#56033). KV groups overlay one row and must stay separate: each group
464 + owns a different block-id space. A run that starts at row offset 0 is
465 + lifted to the full row so consecutive blocks can coalesce.
466 + """
467 + if len(local_regions) != len(remote_regions):
468 + return local_regions, remote_regions
469 +
470 + def adjacent(
471 + run_start: TransferRegion, prev: TransferRegion, nxt: TransferRegion
472 + ) -> bool:
473 + # Page padding sits between unpadded payloads. Allow that gap, but
474 + # keep the whole run inside the row that `run_start` began. Anchoring
475 + # on `prev` would slide the bound forward as the run grows.
476 + return (
477 + prev.group_index == nxt.group_index
478 + and prev.shared_group_ids == nxt.shared_group_ids
479 + and prev.block_len == nxt.block_len
480 + and prev.base_addr + prev.kv_block_len <= nxt.base_addr
481 + and nxt.base_addr + nxt.kv_block_len
482 + <= run_start.base_addr + run_start.block_len
483 + )
484 +
485 + def span(region: TransferRegion, kv_block_len: int) -> TransferRegion:
486 + return TransferRegion(
487 + layer_name=region.layer_name,
488 + layer_index=region.layer_index,
489 + base_addr=region.base_addr,
490 + block_len=region.block_len,
491 + kv_block_len=kv_block_len,
492 + group_index=region.group_index,
493 + shared_group_ids=region.shared_group_ids,
494 + row_offset=region.row_offset,
495 + )
496 +
497 + def promote(
498 + merged_local: list[TransferRegion], merged_remote: list[TransferRegion]
499 + ) -> tuple[list[TransferRegion], list[TransferRegion]]:
500 + if not promote_full_row:
501 + return merged_local, merged_remote
502 + # Each KV group overlays the row from offset 0. Promoting that run to
503 + # the full row copies bytes the group's block ids exclusively own.
504 + # Equal block_len is the row-structure check: both peers packed the
505 + # same stride, so row_offset does not need a second compare in align.
506 + promoted_local: list[TransferRegion] = []
507 + promoted_remote: list[TransferRegion] = []
508 + for local_region, remote_region in zip(merged_local, merged_remote):
509 + if (
510 + local_region.row_offset == 0
511 + and remote_region.row_offset == 0
512 + and local_region.block_len == remote_region.block_len
513 + and local_region.kv_block_len <= local_region.block_len
514 + and remote_region.kv_block_len <= remote_region.block_len
515 + ):
516 + promoted_local.append(span(local_region, local_region.block_len))
517 + promoted_remote.append(span(remote_region, remote_region.block_len))
518 + else:
519 + promoted_local.append(local_region)
520 + promoted_remote.append(remote_region)
521 + return promoted_local, promoted_remote
522 +
523 + if len(local_regions) <= 1:
524 + return promote(list(local_regions), list(remote_regions))
525 +
526 + merged_local: list[TransferRegion] = []
527 + merged_remote: list[TransferRegion] = []
528 + run_local = [local_regions[0]]
529 + run_remote = [remote_regions[0]]
530 +
531 + def flush() -> None:
532 + if len(run_local) == 1:
533 + merged_local.append(run_local[0])
534 + merged_remote.append(run_remote[0])
535 + return
536 + for merged, run in ((merged_local, run_local), (merged_remote, run_remote)):
537 + span_len = run[-1].base_addr + run[-1].kv_block_len - run[0].base_addr
538 + merged.append(span(run[0], span_len))
539 +
540 + for local_region, remote_region in zip(local_regions[1:], remote_regions[1:]):
541 + if adjacent(run_local[0], run_local[-1], local_region) and adjacent(
542 + run_remote[0], run_remote[-1], remote_region
543 + ):
544 + run_local.append(local_region)
545 + run_remote.append(remote_region)
546 + continue
547 + flush()
548 + run_local = [local_region]
549 + run_remote = [remote_region]
550 + flush()
551 + return promote(merged_local, merged_remote)
552 +
553 +
554 +def _has_opaque_packed_row(
555 + row_offsets: list[int], kv_block_lens: list[int], block_lens: list[int]
556 +) -> bool:
557 + """Branch B is one row-sized region at offset 0, so later layer names are gone.
558 +
559 + Page-contiguous views keep kv_block_len below the stride until promotion,
560 + and promotion happens after this handshake check.
561 + """
562 + if (
563 + not row_offsets
564 + or len(row_offsets) != len(kv_block_lens)
565 + or len(row_offsets) != len(block_lens)
566 + ):
567 + return False
568 + return any(
569 + offset == 0 and length == stride
570 + for offset, length, stride in zip(row_offsets, kv_block_lens, block_lens)
571 + )
572 +
573 +
574 +def _pp_mismatch_hides_packed_layers(
575 + local_pp_size: int,
576 + remote_pp_size: int,
577 + *,
578 + local_has_opaque_row: bool,
579 + remote_row_offsets: list[int],
580 + remote_kv_block_lens: list[int],
581 + remote_block_lens: list[int],
582 +) -> bool:
583 + """A row that dropped per-layer names cannot be aligned across PP sizes.
584 +
585 + Either side may be the one that collapsed the row. Matching PP still
586 + lines up, because both stages keep the same first layer name.
587 + """
588 + if local_pp_size == remote_pp_size:
589 + return False
590 + return local_has_opaque_row or _has_opaque_packed_row(
591 + remote_row_offsets, remote_kv_block_lens, remote_block_lens
592 + )
593 +
594 +
595 def _get_tensor_dense_flag(tensor: torch.Tensor) -> bool | None:
596 is_dense = getattr(tensor, "is_non_overlapping_and_dense", None)
597 if callable(is_dense):
@@ -405,6 +614,12 @@ class MooncakeXferMetadata(
614 registered_layer_names: list[str] = msgspec.field(default_factory=list)
615 registered_layer_indices: list[int] = msgspec.field(default_factory=list)
616 registered_group_indices: list[int] = msgspec.field(default_factory=list)
617 + # Parallel to registered regions. Each entry is the groups that share
618 + # that region. Empty means the peer did not send the field.
619 + registered_shared_group_ids: list[list[int]] = msgspec.field(default_factory=list)
620 + # Byte offset of each registered view from its block row. -1 means the
621 + # peer did not send the field, so the run must not be promoted.
622 + registered_row_offsets: list[int] = msgspec.field(default_factory=list)
623 remote_pp_size: int = 1
624
625
@@ -999,7 +1214,18 @@ class MooncakeConnectorWorker:
1214 self.registered_layer_names: list[str] = []
1215 self.registered_layer_indices: list[int] = []
1216 self.registered_group_indices: list[int] = []
1217 + # Parallel to registered regions. -1 flattens only these groups.
1218 + self.region_shared_groups: list[tuple[int, ...]] = []
1219 + # Parallel to registered regions. -1 is not a packed row view.
1220 + self.region_row_offsets: list[int] = []
1221 + self.opaque_packed_storages: set[int] = set()
1222 self.seen_base_addresses: list[int] = []
1223 + # Aligned regions depend only on the peer's registered layout.
1224 + # The third item is an error string when alignment cannot proceed.
1225 + self._prepared_transfer_regions: dict[
1226 + tuple,
1227 + tuple[list[TransferRegion], list[TransferRegion], str | None],
1228 + ] = {}
1229
1230 assert (parallel_config := vllm_config.parallel_config)
1231 dp_rank = parallel_config.data_parallel_index
@@ -1266,39 +1492,35 @@ class MooncakeConnectorWorker:
1492 )
1493 await sock.send_multipart((identity, self._encoder.encode(response)))
1494 return
1269 - local_regions = self._get_transfer_regions(
1270 - self.kv_caches_base_addr,
1271 - self.block_len_per_layer,
1272 - self.kv_block_len_per_layer,
1273 - self.registered_layer_names,
1274 - self.registered_layer_indices,
1275 - self.registered_group_indices,
1276 - )
1277 - remote_regions = self._get_transfer_regions(
1278 - meta.kv_caches_base_addr,
1279 - meta.block_lens,
1280 - meta.kv_block_lens,
1281 - meta.registered_layer_names,
1282 - meta.registered_layer_indices,
1283 - meta.registered_group_indices,
1284 - )
1285 - local_regions, remote_regions, align_err = _align_transfer_regions(
1286 - local_regions,
1287 - remote_regions,
1288 - allow_partial_layers=(
1289 - meta.remote_pp_size > 1 and meta.remote_pp_size != self.pp_size
1290 - ),
1291 - )
1292 - if align_err is not None:
1495 + if _pp_mismatch_hides_packed_layers(
1496 + self.pp_size,
1497 + meta.remote_pp_size,
1498 + local_has_opaque_row=bool(self.opaque_packed_storages),
1499 + remote_row_offsets=meta.registered_row_offsets,
1500 + remote_kv_block_lens=meta.kv_block_lens,
1501 + remote_block_lens=meta.block_lens,
1502 + ):
1503 + msg = (
1504 + "Mooncake non-page-contiguous packed regions keep only the "
1505 + "first layer name, so they cannot be aligned across PP sizes "
1506 + f"{self.pp_size} and {meta.remote_pp_size}."
1507 + )
1508 + logger.error(msg)
1509 response = MooncakeXferResponse(
1510 status=MooncakeXferResponseStatus.ERROR,
1295 - err_msg=align_err,
1511 + err_msg=msg,
1512 )
1513 await sock.send_multipart((identity, self._encoder.encode(response)))
1514 return
1299 - validation_err = self._validate_head_resharding_layout(
1300 - meta.remote_tp_size, local_regions
1301 - ) or _validate_asymmetric_region_lengths(
1515 + local_regions, remote_regions, prep_err = self._prepare_transfer_regions(meta)
1516 + if prep_err is not None:
1517 + response = MooncakeXferResponse(
1518 + status=MooncakeXferResponseStatus.ERROR,
1519 + err_msg=prep_err,
1520 + )
1521 + await sock.send_multipart((identity, self._encoder.encode(response)))
1522 + return
1523 + validation_err = _validate_asymmetric_region_lengths(
1524 local_regions=local_regions,
1525 remote_regions=remote_regions,
1526 local_tp_size=self.tp_size,
@@ -1600,12 +1822,16 @@ class MooncakeConnectorWorker:
1822 "Aligned Mooncake transfer regions must belong to the same "
1823 "KV group."
1824 )
1603 - group_index = local_region.group_index
1604 - assert group_index < len(local_block_ids_by_group), (
1605 - "Transfer region references a missing KV group."
1825 + local_block_ids = _block_ids_for_region(
1826 + local_block_ids_by_group,
1827 + local_region.group_index,
1828 + local_region.shared_group_ids,
1829 + )
1830 + remote_block_ids = _block_ids_for_region(
1831 + remote_block_ids_by_group,
1832 + remote_region.group_index,
1833 + local_region.shared_group_ids,
1834 )
1607 - local_block_ids = local_block_ids_by_group[group_index]
1608 - remote_block_ids = remote_block_ids_by_group[group_index]
1835 if not local_block_ids:
1836 continue
1837
@@ -1738,6 +1964,17 @@ class MooncakeConnectorWorker:
1964 self.registered_layer_names = []
1965 self.registered_layer_indices = []
1966 self.registered_group_indices = []
1967 + self.region_shared_groups = []
1968 + self.region_row_offsets = []
1969 + self.opaque_packed_storages = set()
1970 + self._prepared_transfer_regions.clear()
1971 +
1972 + packed_storage_to_region: dict[int, int] = {}
1973 + packed_view_to_region: dict[tuple[int, int, int], int] = {}
1974 + collapsed_views = 0
1975 + num_blocks = (
1976 + self.kv_cache_config.num_blocks * self._physical_blocks_per_logical_kv_block
1977 + )
1978
1979 for layer_name, cache in kv_caches.items():
1980 layer_index = extract_layer_index(layer_name)
@@ -1751,6 +1988,104 @@ class MooncakeConnectorWorker:
1988 # One raw page tensor per layer; for Mamba that page holds all the
1989 # recurrent states, unpacked only when binding the cache for execution.
1990 self._log_debug_cache_registration(layer_name, cache)
1991 + group_index = self.kv_cache_config.transfer_group_index_by_layer[layer_name]
1992 + storage = cache.untyped_storage()
1993 + storage_addr = storage.data_ptr()
1994 + if storage_addr not in seen_storage_ptrs:
1995 + seen_storage_ptrs.add(storage_addr)
1996 + kv_data_ptrs.append(storage_addr)
1997 + kv_data_lens.append(storage.nbytes())
1998 +
1999 + is_mla_region = isinstance(
2000 + layer_spec, (MLAAttentionSpec, SlidingWindowMLASpec)
2001 + )
2002 + tensor_blocks = cache.shape[0] if cache.ndim > 1 else num_blocks
2003 + if cache.ndim == 1:
2004 + # A flat buffer is block-major only when it is an integral run
2005 + # of this layer's pages. NIXL uses the same remainder check.
2006 + hnc_contiguous = False
2007 + page = (
2008 + layer_spec.page_size_bytes
2009 + if isinstance(layer_spec, AttentionSpec)
2010 + else 0
2011 + )
2012 + remainder = (
2013 + storage.nbytes() - tensor_blocks * page
2014 + if tensor_blocks > 0 and page > 0
2015 + else -1
2016 + )
2017 + if 0 <= remainder < page:
2018 + block_stride = page
2019 + storage_is_block_major = True
2020 + else:
2021 + block_stride = 0
2022 + storage_is_block_major = False
2023 + else:
2024 + block_stride = cache.stride(0) * cache.element_size()
2025 + storage_is_block_major = (
2026 + tensor_blocks > 0
2027 + and tensor_blocks * block_stride == storage.nbytes()
2028 + )
2029 + hnc_contiguous = (
2030 + cache.ndim == 4
2031 + and cache.stride(2) == cache.shape[3]
2032 + and cache.stride(1) == cache.shape[2] * cache.shape[3]
2033 + )
2034 + # Follow NIXL packed pages. Page-contiguous slices stay per layer so
2035 + # hetero PP can align by name (#56033). A non-contiguous row cannot
2036 + # name a sub-span, so it is one region anchored at the allocation.
2037 + use_packed_row = (
2038 + not isinstance(layer_spec, (MambaSpec, KpoolTailSpec))
2039 + and storage_is_block_major
2040 + and block_stride > 0
2041 + and (is_mla_region or not hnc_contiguous)
2042 + )
2043 + if use_packed_row:
2044 + page_contiguous = (
2045 + cache.ndim > 1
2046 + and cache[0].is_contiguous()
2047 + and 0 < cache[0].nbytes <= block_stride
2048 + )
2049 + if page_contiguous:
2050 + view_addr = cache.data_ptr()
2051 + kv_block_len = cache[0].nbytes
2052 + else:
2053 + # NIXL branch B: length is the row, base is the allocation.
2054 + view_addr = storage_addr
2055 + kv_block_len = block_stride
2056 + self.opaque_packed_storages.add(storage_addr)
2057 + # Same PP on both sides still aligns by the first layer name.
2058 + # A remote PP mismatch is rejected at send time, once the
2059 + # peer's PP size is known.
2060 + dedupe_key = (view_addr, block_stride, kv_block_len)
2061 + region_idx = packed_view_to_region.get(dedupe_key)
2062 + if region_idx is not None:
2063 + collapsed_views += 1
2064 + # -1 means these groups share the view. Flatten only them.
2065 + groups = list(self.region_shared_groups[region_idx])
2066 + if group_index not in groups:
2067 + groups.append(group_index)
2068 + self.region_shared_groups[region_idx] = tuple(groups)
2069 + if len(self.region_shared_groups[region_idx]) > 1:
2070 + self.registered_group_indices[region_idx] = (
2071 + _SHARED_REGION_GROUP_ID
2072 + )
2073 + continue
2074 + if not page_contiguous:
2075 + self.opaque_packed_storages.add(storage_addr)
2076 + if storage_addr not in packed_storage_to_region:
2077 + packed_storage_to_region[storage_addr] = len(region_base_addresses)
2078 + packed_view_to_region[dedupe_key] = len(region_base_addresses)
2079 + region_base_addresses.append(view_addr)
2080 + self.block_len_per_layer.append(block_stride)
2081 + self.kv_block_len_per_layer.append(kv_block_len)
2082 + self.registered_layer_names.append(layer_name)
2083 + self.registered_layer_indices.append(layer_index)
2084 + self.registered_group_indices.append(group_index)
2085 + self.region_shared_groups.append((group_index,))
2086 + self.region_row_offsets.append(view_addr - storage_addr)
2087 + continue
2088 +
2089 block_is_contiguous = is_non_overlapping_and_dense(cache[0])
2090 if not block_is_contiguous:
2091 # Non-block-compact layouts scatter a block across per-head
@@ -1791,15 +2126,10 @@ class MooncakeConnectorWorker:
2126 self.kv_block_len_per_layer.append(kv_block_len)
2127 self.registered_layer_names.append(layer_name)
2128 self.registered_layer_indices.append(layer_index)
1794 - self.registered_group_indices.append(
1795 - self.kv_cache_config.transfer_group_index_by_layer[layer_name]
1796 - )
1797 - storage = cache.untyped_storage()
1798 - storage_addr = storage.data_ptr()
1799 - if storage_addr not in seen_storage_ptrs:
1800 - seen_storage_ptrs.add(storage_addr)
1801 - kv_data_ptrs.append(storage_addr)
1802 - kv_data_lens.append(storage.nbytes())
2129 + self.registered_group_indices.append(group_index)
2130 + self.region_shared_groups.append((group_index,))
2131 + # Not a view into a shared packed row, so do not promote it.
2132 + self.region_row_offsets.append(-1)
2133
2134 self.kv_caches_base_addr = region_base_addresses
2135 self.seen_base_addresses = kv_data_ptrs
@@ -1807,6 +2137,18 @@ class MooncakeConnectorWorker:
2137 if not kv_data_ptrs:
2138 raise RuntimeError("No KV cache tensors were registered with Mooncake.")
2139
2140 + if packed_storage_to_region and (
2141 + collapsed_views
2142 + or len(packed_view_to_region) != len(packed_storage_to_region)
2143 + ):
2144 + logger.info(
2145 + "Mooncake packed KV xfer: %s layer regions over %s storage "
2146 + "regions (%s duplicate views collapsed).",
2147 + len(packed_view_to_region),
2148 + len(packed_storage_to_region),
2149 + collapsed_views,
2150 + )
2151 +
2152 ret_value = self.engine.batch_register_memory(kv_data_ptrs, kv_data_lens)
2153 if ret_value != 0:
2154 raise RuntimeError("Mooncake batch memory registration failed.")
@@ -1962,6 +2304,10 @@ class MooncakeConnectorWorker:
2304 registered_layer_names=self.registered_layer_names,
2305 registered_layer_indices=self.registered_layer_indices,
2306 registered_group_indices=self.registered_group_indices,
2307 + registered_shared_group_ids=[
2308 + list(groups) for groups in self.region_shared_groups
2309 + ],
2310 + registered_row_offsets=self.region_row_offsets,
2311 )
2312
2313 encoded_data = self._encoder.encode(metadata)
@@ -2209,6 +2555,131 @@ class MooncakeConnectorWorker:
2555 def _producer_cache_is_replicated(self) -> bool:
2556 return self.transfer_topo.local_replicates_kv_cache
2557
2558 + def _prepare_transfer_regions(
2559 + self, meta: MooncakeXferMetadata
2560 + ) -> tuple[list[TransferRegion], list[TransferRegion], str | None]:
2561 + """Align and coalesce once per peer layout.
2562 +
2563 + ``send_kv_to_decode`` runs on every scheduler batch. The registered
2564 + layout does not change until the next ``register_kv_caches``.
2565 + """
2566 + cache_key = (
2567 + meta.remote_hostname,
2568 + meta.remote_port,
2569 + meta.remote_tp_size,
2570 + meta.remote_tp_rank,
2571 + meta.remote_pp_size,
2572 + tuple(meta.kv_caches_base_addr),
2573 + tuple(meta.block_lens),
2574 + tuple(meta.kv_block_lens),
2575 + tuple(meta.registered_layer_names),
2576 + tuple(meta.registered_layer_indices),
2577 + tuple(meta.registered_group_indices),
2578 + tuple(tuple(groups) for groups in meta.registered_shared_group_ids),
2579 + tuple(meta.registered_row_offsets),
2580 + )
2581 + cached = self._prepared_transfer_regions.get(cache_key)
2582 + if cached is not None:
2583 + return cached
2584 +
2585 + def finish(
2586 + local: list[TransferRegion],
2587 + remote: list[TransferRegion],
2588 + err: str | None,
2589 + ) -> tuple[list[TransferRegion], list[TransferRegion], str | None]:
2590 + prepared = (local, remote, err)
2591 + self._prepared_transfer_regions[cache_key] = prepared
2592 + return prepared
2593 +
2594 + shared_groups: list[tuple[int, ...]] | None = self.region_shared_groups
2595 + if len(self.region_shared_groups) != len(self.registered_layer_names):
2596 + if _SHARED_REGION_GROUP_ID in self.registered_group_indices:
2597 + return finish(
2598 + [],
2599 + [],
2600 + (
2601 + "Mooncake shared-group metadata does not match "
2602 + "registered layers, so a shared packed region cannot "
2603 + "be transferred."
2604 + ),
2605 + )
2606 + shared_groups = None
2607 + local_rows: list[int] | None = self.region_row_offsets
2608 + if len(self.region_row_offsets) != len(self.registered_layer_names):
2609 + local_rows = None
2610 + local_regions = self._get_transfer_regions(
2611 + self.kv_caches_base_addr,
2612 + self.block_len_per_layer,
2613 + self.kv_block_len_per_layer,
2614 + self.registered_layer_names,
2615 + self.registered_layer_indices,
2616 + self.registered_group_indices,
2617 + shared_groups,
2618 + local_rows,
2619 + )
2620 + remote_shared_groups = [
2621 + tuple(groups) for groups in meta.registered_shared_group_ids
2622 + ]
2623 + remote_shared: list[tuple[int, ...]] | None = remote_shared_groups
2624 + if len(remote_shared_groups) != len(meta.registered_layer_names):
2625 + remote_shared = None
2626 + remote_rows = meta.registered_row_offsets
2627 + remote_row_offsets: list[int] | None = remote_rows
2628 + if len(remote_rows) != len(meta.registered_layer_names):
2629 + remote_row_offsets = None
2630 + remote_regions = self._get_transfer_regions(
2631 + meta.kv_caches_base_addr,
2632 + meta.block_lens,
2633 + meta.kv_block_lens,
2634 + meta.registered_layer_names,
2635 + meta.registered_layer_indices,
2636 + meta.registered_group_indices,
2637 + remote_shared,
2638 + remote_row_offsets,
2639 + )
2640 + pre_align_local = local_regions
2641 + pre_align_remote = remote_regions
2642 + local_regions, remote_regions, align_err = _align_transfer_regions(
2643 + local_regions,
2644 + remote_regions,
2645 + allow_partial_layers=meta.remote_pp_size != self.pp_size,
2646 + )
2647 + if align_err is not None:
2648 + return finish([], [], align_err)
2649 + # Head checks must see every layer name. Coalesce keeps only the first.
2650 + head_err = self._validate_head_resharding_layout(
2651 + meta.remote_tp_size, local_regions
2652 + )
2653 + if head_err is not None:
2654 + return finish([], [], head_err)
2655 + # Hetero TP indexes kv_block_len as one head-sharded payload. A merged
2656 + # multi-layer span would land that offset on the wrong layer.
2657 + # Replicated KV keeps that offset at 0. MLA is replicated in practice;
2658 + # gating on use_mla alone would promote and then fail the length check
2659 + # for a non-replicated MLA rank with tp_ratio != 1.
2660 + tp_ratio = _get_tp_ratio(self.tp_size, meta.remote_tp_size)
2661 + if tp_ratio == 1 or self._producer_cache_is_replicated():
2662 + covered_both_sides = len(local_regions) == len(pre_align_local) and len(
2663 + remote_regions
2664 + ) == len(pre_align_remote)
2665 + n_aligned = len(local_regions)
2666 + local_regions, remote_regions = _coalesce_contiguous_transfer_regions(
2667 + local_regions,
2668 + remote_regions,
2669 + promote_full_row=covered_both_sides,
2670 + )
2671 + logger.info(
2672 + "Mooncake packed coalesce: %s aligned regions -> %s transfer regions.",
2673 + n_aligned,
2674 + len(local_regions),
2675 + )
2676 + self._prepared_transfer_regions[cache_key] = (
2677 + local_regions,
2678 + remote_regions,
2679 + None,
2680 + )
2681 + return local_regions, remote_regions, None
2682 +
2683 def _validate_head_resharding_layout(
2684 self, remote_tp_size: int, local_regions: list[TransferRegion]
2685 ) -> str | None:
@@ -2242,6 +2713,8 @@ class MooncakeConnectorWorker:
2713 layer_names: list[str],
2714 layer_indices: list[int],
2715 group_indices: list[int] | None = None,
2716 + shared_group_ids: list[tuple[int, ...]] | None = None,
2717 + row_offsets: list[int] | None = None,
2718 ) -> list[TransferRegion]:
2719 if not group_indices:
2720 group_indices = [
@@ -2255,6 +2728,8 @@ class MooncakeConnectorWorker:
2728 layer_names=layer_names,
2729 layer_indices=layer_indices,
2730 group_indices=group_indices,
2731 + shared_group_ids=shared_group_ids,
2732 + row_offsets=row_offsets,
2733 )
2734
2735 def _get_sender_transfer_plan(