[Bugfix][KV Connector][Mooncake] Keep TP-sharded Mamba state out of the KV-head dedup (#49499)
Signed-off-by: Yifan Qiao <yifanqiao@inferact.ai> Co-authored-by: Claude <noreply@anthropic.com>
Yifan Qiao committed
Jul 24, 2026 at 21:02 UTC
70052fb924fbd633580308562d43b9df838ddf6d
3 files changed
+311
-93
tests/v1/kv_connector/unit/test_mooncake_store_hma_e2e.py
+1
-2
@@ -173,7 +173,6 @@ def test_e2e_swa_plus_full_save_then_lookup_hits():
173
worker = _build_worker_with_dict_store(vllm_config, cfg, store)
174
worker.tp_size = 1
175
worker.pp_size = 1
176
- worker.put_step = 1
176
worker.num_kv_head = 8
177
178
# Register kv_caches using mocked thread classes so register_kv_caches
@@ -215,7 +214,7 @@ def test_e2e_swa_plus_full_save_then_lookup_hits():
214
block_size=worker.block_size,
215
coord=worker.coord,
216
tp_rank=worker.tp_rank,
218
- put_step=worker.put_step,
217
+ group_put_steps=worker._group_tp_replication_factors,
218
kv_role=worker.kv_role,
219
ready_event=ready,
220
enable_kv_event=False,
tests/v1/kv_connector/unit/test_mooncake_store_worker.py
+213
-13
@@ -90,7 +90,7 @@ def _make_store_sending_thread(
90
block_size=block_size,
91
coord=coord,
92
tp_rank=tp_rank,
93
- put_step=put_step,
93
+ group_put_steps=[put_step] * len(token_databases),
94
kv_role="kv_producer",
95
ready_event=threading.Event(),
96
replicate_config=replicate_config,
@@ -527,6 +527,29 @@ def test_store_sending_thread_delta_strides_with_local_phase():
527
assert store.batch_put_from_multi_buffers.call_args.args[0] == keys
528
529
530
+def test_tp_sharded_group_saves_every_block_on_every_rank():
531
+ """Sharded ranks must write every block because peers hold different bytes."""
532
+ store = MagicMock()
533
+ store.batch_is_exist.side_effect = lambda keys: [0] * len(keys)
534
+ store.batch_put_from_multi_buffers.side_effect = lambda keys, *a: [256] * len(keys)
535
+ thread = _make_store_sending_thread(store, tp_rank=0, put_step=2)
536
+ thread.group_put_steps = [1]
537
+
538
+ thread.add_stored_request("req-a")
539
+ thread._handle_request(
540
+ ReqMeta(
541
+ req_id="req-a",
542
+ token_len_chunk=64,
543
+ block_ids=([0, 1, 2, 3],),
544
+ block_hashes=[b"a0", b"a1", b"a2", b"a3"],
545
+ can_save=True,
546
+ )
547
+ )
548
+
549
+ keys = store.batch_is_exist.call_args.args[0]
550
+ assert len(keys) == 4
551
+
552
+
553
def test_store_sending_thread_retries_skipped_range_after_pressure():
554
store = MagicMock()
555
store.batch_is_exist.side_effect = lambda keys: [0] * len(keys)
@@ -1274,7 +1297,8 @@ def test_worker_put_striding_covers_every_rank_get_namespace(
1297
]
1298
assert len(keys) == len(block_hashes)
1299
# PUT side: mirrors KVCacheStoreSendingThread's striding slice.
1277
- put_keys.update(keys[w.tp_rank % w.put_step :: w.put_step])
1300
+ put_step = w._group_tp_replication_factors[0]
1301
+ put_keys.update(keys[w.tp_rank % put_step :: put_step])
1302
# GET side: KVCacheStoreRecvingThread fetches every key.
1303
get_keys_per_rank[tp_rank] = set(keys)
1304
@@ -1613,6 +1637,15 @@ def _register_with_mocked_threads(
1637
worker.register_kv_caches(kv_caches)
1638
1639
1640
+def _refresh_group_tp_replication_factors(
1641
+ worker: mooncake_store_worker.MooncakeStoreWorker,
1642
+) -> None:
1643
+ worker._group_tp_replication_factors = (
1644
+ worker._compute_group_tp_replication_factors()
1645
+ )
1646
+ worker._init_lookup_key_prefixes()
1647
+
1648
+
1649
def _make_bare_worker(
1650
*,
1651
num_gpu_blocks: int = 10,
@@ -1630,11 +1663,9 @@ def _make_bare_worker(
1663
worker.cache_config.num_gpu_blocks = num_gpu_blocks
1664
worker.store = MagicMock()
1665
worker.store.register_buffer.return_value = 0
1633
- worker.use_mla = False
1666
worker.kv_role = kv_role
1667
worker.block_size = block_size
1668
worker.tp_rank = 0
1637
- worker.put_step = 1
1669
worker.enable_kv_events = False
1670
worker.kv_send_thread = None
1671
worker.kv_recv_threads = []
@@ -1664,7 +1695,6 @@ def _make_bare_worker(
1695
worker.pcp_size = 1
1696
worker.dcp_size = 1
1697
worker.hash_block_size = block_size
1667
- worker.metadata = KeyMetadata("test-model", 0, 0, 0, 0)
1698
# Pre-build a single-group token_dbs so lookup-only tests don't have to
1699
# call register_kv_caches.
1700
worker.token_dbs = [
@@ -1679,7 +1709,7 @@ def _make_bare_worker(
1709
scheduler_block_size=block_size,
1710
hash_block_size=block_size,
1711
)
1682
- worker._init_lookup_key_prefixes()
1712
+ _refresh_group_tp_replication_factors(worker)
1713
return worker
1714
1715
@@ -1688,9 +1718,8 @@ def test_lookup_key_prefixes_cover_dcp_rank_namespaces():
1718
worker.tp_size = 4
1719
worker.num_kv_head = 1
1720
worker.dcp_size = 4
1691
- worker._init_lookup_key_prefixes()
1721
+ _refresh_group_tp_replication_factors(worker)
1722
1693
- assert worker._lookup_expected_per_key == 4
1723
assert worker._lookup_key_prefixes[0] == (
1724
"test-model@tp_rank:0@pcp0@dcp0@pp_rank:0@group:0",
1725
"test-model@tp_rank:1@pcp0@dcp1@pp_rank:0@group:0",
@@ -1705,21 +1734,192 @@ def test_lookup_key_prefixes_cover_pcp_rank_namespaces():
1734
worker.num_kv_head = 1
1735
worker.pcp_size = 2
1736
worker.dcp_size = 1
1708
- worker._init_lookup_key_prefixes()
1737
+ _refresh_group_tp_replication_factors(worker)
1738
1710
- assert worker._lookup_expected_per_key == 2
1739
assert worker._lookup_key_prefixes[0] == (
1740
"test-model@tp_rank:0@pcp0@dcp0@pp_rank:0@group:0",
1741
"test-model@tp_rank:0@pcp1@dcp0@pp_rank:0@group:0",
1742
)
1743
1744
1745
+def test_lookup_key_prefixes_expand_tp_sharded_groups_per_rank():
1746
+ """Replicated attention needs one namespace; sharded Mamba needs every rank."""
1747
+ from vllm.v1.kv_cache_interface import (
1748
+ FullAttentionSpec,
1749
+ KVCacheGroupSpec,
1750
+ MambaSpec,
1751
+ )
1752
+
1753
+ worker = _make_bare_worker(block_size=16)
1754
+ worker.tp_size = 2
1755
+ worker.num_kv_head = 1
1756
+ fa = FullAttentionSpec(block_size=16, num_kv_heads=8, head_size=64, dtype=None)
1757
+ mamba = MambaSpec(
1758
+ block_size=16,
1759
+ shapes=((1, 1),),
1760
+ dtypes=(torch.float32,),
1761
+ mamba_cache_mode="align",
1762
+ )
1763
+ worker._kv_cache_groups = [
1764
+ KVCacheGroupSpec(["l0"], fa),
1765
+ KVCacheGroupSpec(["l1"], mamba),
1766
+ ]
1767
+ worker.token_dbs = [
1768
+ ChunkedTokenDatabase(
1769
+ KeyMetadata("test-model", 0, 0, 0, 0, group_id=0), block_size=16
1770
+ ),
1771
+ ChunkedTokenDatabase(
1772
+ KeyMetadata("test-model", 1, 0, 0, 0, group_id=1), block_size=16
1773
+ ),
1774
+ ]
1775
+ _refresh_group_tp_replication_factors(worker)
1776
+
1777
+ assert worker._lookup_key_prefixes[0] == (
1778
+ "test-model@tp_rank:0@pcp0@dcp0@pp_rank:0@group:0",
1779
+ )
1780
+ assert worker._lookup_key_prefixes[1] == (
1781
+ "test-model@tp_rank:0@pcp0@dcp0@pp_rank:0@group:1",
1782
+ "test-model@tp_rank:1@pcp0@dcp0@pp_rank:0@group:1",
1783
+ )
1784
+
1785
+
1786
+def test_group_tp_replication_factors_mixed_mla_gqa_mamba():
1787
+ from vllm.v1.kv_cache_interface import (
1788
+ FullAttentionSpec,
1789
+ KVCacheGroupSpec,
1790
+ MambaSpec,
1791
+ MLAAttentionSpec,
1792
+ )
1793
+
1794
+ worker = _make_bare_worker(block_size=16)
1795
+ worker.tp_size = 4
1796
+ worker.num_kv_head = 2
1797
+ mla = MLAAttentionSpec(block_size=16, num_kv_heads=1, head_size=64, dtype=None)
1798
+ gqa = FullAttentionSpec(block_size=16, num_kv_heads=8, head_size=64, dtype=None)
1799
+ mamba = MambaSpec(
1800
+ block_size=16,
1801
+ shapes=((1, 1),),
1802
+ dtypes=(torch.float32,),
1803
+ mamba_cache_mode="align",
1804
+ )
1805
+ worker._kv_cache_groups = [
1806
+ KVCacheGroupSpec(["l0"], mla),
1807
+ KVCacheGroupSpec(["l1"], gqa),
1808
+ KVCacheGroupSpec(["l2"], mamba),
1809
+ ]
1810
+ worker.token_dbs = [
1811
+ ChunkedTokenDatabase(
1812
+ KeyMetadata("test-model", 0, 0, 0, 0, group_id=g_idx), block_size=16
1813
+ )
1814
+ for g_idx in range(3)
1815
+ ]
1816
+
1817
+ _refresh_group_tp_replication_factors(worker)
1818
+ assert worker._group_tp_replication_factors == (4, 2, 1)
1819
+ assert worker._lookup_key_prefixes[0] == (
1820
+ "test-model@tp_rank:0@pcp0@dcp0@pp_rank:0@group:0",
1821
+ )
1822
+ assert worker._lookup_key_prefixes[1] == (
1823
+ "test-model@tp_rank:0@pcp0@dcp0@pp_rank:0@group:1",
1824
+ "test-model@tp_rank:1@pcp0@dcp0@pp_rank:0@group:1",
1825
+ )
1826
+ assert worker._lookup_key_prefixes[2] == (
1827
+ "test-model@tp_rank:0@pcp0@dcp0@pp_rank:0@group:2",
1828
+ "test-model@tp_rank:1@pcp0@dcp0@pp_rank:0@group:2",
1829
+ "test-model@tp_rank:2@pcp0@dcp0@pp_rank:0@group:2",
1830
+ "test-model@tp_rank:3@pcp0@dcp0@pp_rank:0@group:2",
1831
+ )
1832
+
1833
+
1834
+@pytest.mark.parametrize("spec_order", [("mla", "gqa"), ("gqa", "mla")])
1835
+def test_uniform_group_uses_common_inner_replication_factor(spec_order):
1836
+ from vllm.v1.kv_cache_interface import (
1837
+ FullAttentionSpec,
1838
+ KVCacheGroupSpec,
1839
+ MLAAttentionSpec,
1840
+ UniformTypeKVCacheSpecs,
1841
+ )
1842
+
1843
+ worker = _make_bare_worker(block_size=16)
1844
+ worker.tp_size = 4
1845
+ worker.num_kv_head = 2
1846
+ specs_by_name = {
1847
+ "mla": MLAAttentionSpec(
1848
+ block_size=16, num_kv_heads=1, head_size=64, dtype=None
1849
+ ),
1850
+ "gqa": FullAttentionSpec(
1851
+ block_size=16, num_kv_heads=1, head_size=64, dtype=None
1852
+ ),
1853
+ }
1854
+ inner_specs = {name: specs_by_name[name] for name in spec_order}
1855
+ uniform_spec = UniformTypeKVCacheSpecs(
1856
+ block_size=16,
1857
+ kv_cache_specs=inner_specs,
1858
+ )
1859
+ worker._kv_cache_groups = [
1860
+ KVCacheGroupSpec(list(inner_specs), uniform_spec),
1861
+ ]
1862
+
1863
+ _refresh_group_tp_replication_factors(worker)
1864
+
1865
+ assert worker._group_tp_replication_factors == (2,)
1866
+ assert worker._lookup_key_prefixes[0] == (
1867
+ "test-model@tp_rank:0@pcp0@dcp0@pp_rank:0@group:0",
1868
+ "test-model@tp_rank:1@pcp0@dcp0@pp_rank:0@group:0",
1869
+ )
1870
+
1871
+
1872
+def test_lookup_rejects_boundary_missing_one_mamba_shard():
1873
+ from vllm.v1.kv_cache_interface import (
1874
+ FullAttentionSpec,
1875
+ KVCacheGroupSpec,
1876
+ MambaSpec,
1877
+ )
1878
+
1879
+ worker = _make_bare_worker(block_size=16)
1880
+ worker.tp_size = 2
1881
+ worker.num_kv_head = 1
1882
+ fa = FullAttentionSpec(block_size=16, num_kv_heads=8, head_size=64, dtype=None)
1883
+ mamba = MambaSpec(
1884
+ block_size=16,
1885
+ shapes=((1, 1),),
1886
+ dtypes=(torch.float32,),
1887
+ mamba_cache_mode="align",
1888
+ )
1889
+ worker._kv_cache_groups = [
1890
+ KVCacheGroupSpec(["l0"], fa),
1891
+ KVCacheGroupSpec(["l1"], mamba),
1892
+ ]
1893
+ worker.token_dbs = [
1894
+ ChunkedTokenDatabase(
1895
+ KeyMetadata("test-model", 0, 0, 0, 0, group_id=0), block_size=16
1896
+ ),
1897
+ ChunkedTokenDatabase(
1898
+ KeyMetadata("test-model", 1, 0, 0, 0, group_id=1), block_size=16
1899
+ ),
1900
+ ]
1901
+ worker.coord = mooncake_store_worker.MooncakeStoreCoordinator(
1902
+ worker._kv_cache_groups,
1903
+ scheduler_block_size=16,
1904
+ hash_block_size=16,
1905
+ )
1906
+ _refresh_group_tp_replication_factors(worker)
1907
+
1908
+ worker.store.batch_is_exist.side_effect = lambda keys: [1] * len(keys)
1909
+ assert worker.lookup(32, [b"h0", b"h1"]) == 32
1910
+
1911
+ worker.store.batch_is_exist.side_effect = lambda keys: [
1912
+ 0 if "tp_rank:1" in k and "group:1" in k else 1 for k in keys
1913
+ ]
1914
+ assert worker.lookup(32, [b"h0", b"h1"]) == 0
1915
+
1916
+
1917
def test_lookup_requires_all_dcp_rank_namespaces():
1918
worker = _make_bare_worker(block_size=16)
1919
worker.tp_size = 4
1920
worker.num_kv_head = 1
1921
worker.dcp_size = 4
1722
- worker._init_lookup_key_prefixes()
1922
+ _refresh_group_tp_replication_factors(worker)
1923
worker.store.batch_is_exist.return_value = [1, 1, 0, 1]
1924
1925
assert worker.lookup(16, [b"a0"]) == 0
@@ -1850,7 +2050,7 @@ def test_lookup_checks_all_potential_swa_hit_boundaries():
2050
hash_block_size=8,
2051
retention_interval=0,
2052
)
1853
- worker._init_lookup_key_prefixes()
2053
+ _refresh_group_tp_replication_factors(worker)
2054
# Candidate order: 3 full-attention chunks, then SWA chunks 3, 7, 11.
2055
# Only the first full chunk and the SWA chunk ending at token 32 exist, so
2056
# lookup should recover a 32-token external prefix hit. A sparse
@@ -1911,7 +2111,7 @@ def test_lookup_applies_swa_mask_before_accessing_hashes():
2111
hash_block_size=8,
2112
retention_interval=0,
2113
)
1914
- worker._init_lookup_key_prefixes()
2114
+ _refresh_group_tp_replication_factors(worker)
2115
2116
block_hashes = _RecordingBlockHashes([f"h{i}".encode() for i in range(12)])
2117
accessed_before_rpc: list[int] = []
vllm/distributed/kv_transfer/kv_connector/v1/mooncake/store/worker.py
+97
-78
@@ -66,7 +66,15 @@ from vllm.v1.core.kv_cache_utils import (
66
maybe_convert_block_hash,
67
resolve_kv_cache_block_sizes,
68
)
69
-from vllm.v1.kv_cache_interface import KVCacheConfig, KVCacheGroupSpec
69
+from vllm.v1.kv_cache_interface import (
70
+ KVCacheConfig,
71
+ KVCacheGroupSpec,
72
+ KVCacheSpec,
73
+ MambaSpec,
74
+ MLAAttentionSpec,
75
+ SlidingWindowMLASpec,
76
+ UniformTypeKVCacheSpecs,
77
+)
78
79
from .metrics import MooncakeStoreConnectorStats
80
@@ -448,7 +456,7 @@ class KVCacheStoreSendingThread(KVTransferThread):
456
token_databases: list[ChunkedTokenDatabase],
457
block_size: int,
458
tp_rank: int,
451
- put_step: int,
459
+ group_put_steps: Sequence[int],
460
kv_role: str,
461
ready_event: threading.Event,
462
enable_kv_event: bool = False,
@@ -464,7 +472,8 @@ class KVCacheStoreSendingThread(KVTransferThread):
472
name="KVCacheStoreSendingThread",
473
record_operation=record_operation,
474
)
467
- self.put_step = put_step
475
+ # Only ranks with identical group bytes may stripe PUTs (e.g., MLA).
476
+ self.group_put_steps = group_put_steps
477
self.coord = coord
478
self.kv_role = kv_role
479
self.stored_requests: defaultdict[str, int] = defaultdict(int)
@@ -568,13 +577,14 @@ class KVCacheStoreSendingThread(KVTransferThread):
577
group_indices: list[int] = []
578
for g_idx, db in enumerate(self.token_databases):
579
# Rotate the stride phase per group to balance load across ranks.
571
- put_step_rank = (self.tp_rank + g_idx) % self.put_step
580
+ put_step = self.group_put_steps[g_idx]
581
+ put_step_rank = (self.tp_rank + g_idx) % put_step
582
for start, end, block_hash in db.process_tokens(
583
token_len,
584
req_meta.block_hashes,
585
mask_num=save_start,
586
chunk_mask=store_masks[g_idx],
577
- put_step=self.put_step,
587
+ put_step=put_step,
588
put_step_rank=put_step_rank,
589
):
590
starts.append(start)
@@ -1002,44 +1012,7 @@ class MooncakeStoreWorker:
1012
)
1013
self.num_layers = model_config.get_num_layers(parallel_config)
1014
1005
- self.use_mla = False
1006
- if (
1007
- hasattr(model_config, "use_mla")
1008
- and isinstance(model_config.use_mla, bool)
1009
- and model_config.use_mla
1010
- ):
1011
- self.use_mla = True
1012
-
1013
- if self.use_mla:
1014
- self.num_kv_head = 1
1015
- else:
1016
- self.num_kv_head = model_config.get_total_num_kv_heads()
1017
-
1018
- if self.num_kv_head < self.tp_size and self.dcp_size <= 1:
1019
- # Dedup: TP ranks holding the same KV heads stripe PUTs across
1020
- # one shared key namespace. DCP splits the TP group, so with
1021
- # DCP>1 those ranks have different `@dcpN` namespaces and
1022
- # striping would leave keys unwritten (OBJECT_NOT_FOUND on
1023
- # GET). PCP is outer to TP (pcp_rank is constant within a TP
1024
- # group), so it needs no guard.
1025
- self.put_step = self.tp_size // self.num_kv_head
1026
- self.head_or_tp_rank = self.tp_rank // self.put_step
1027
- else:
1028
- self.head_or_tp_rank = self.tp_rank
1029
- self.put_step = 1
1030
-
1031
- self.metadata = KeyMetadata(
1032
- model_name=model_config.model.rstrip("/").split("/")[-1],
1033
- tp_rank=self.head_or_tp_rank,
1034
- pcp_rank=self.pcp_rank,
1035
- dcp_rank=self.dcp_rank,
1036
- pp_rank=self.pp_rank,
1037
- cache_prefix=str(
1038
- vllm_config.kv_transfer_config.kv_connector_extra_config.get(
1039
- "cache_prefix", ""
1040
- )
1041
- ),
1042
- )
1015
+ self.num_kv_head = model_config.get_total_num_kv_heads()
1016
1017
# Initialize MooncakeDistributedStore with its own TransferEngine
1018
store_config = MooncakeStoreConfig.load_from_config()
@@ -1156,10 +1129,32 @@ class MooncakeStoreWorker:
1129
retention_interval=envs.VLLM_PREFIX_CACHE_RETENTION_INTERVAL,
1130
)
1131
# One ChunkedTokenDatabase per group; addresses populated in
1159
- # register_kv_caches once the kv-cache layout is known.
1132
+ # register_kv_caches once the kv-cache layout is known. Each group's
1133
+ # key namespace is its TP shard id: ranks holding identical bytes
1134
+ # (MLA / shared GQA KV heads) share a namespace, TP-sharded Mamba
1135
+ # state gets one namespace per rank.
1136
+ metadata = KeyMetadata(
1137
+ model_name=model_config.model.rstrip("/").split("/")[-1],
1138
+ tp_rank=self.tp_rank,
1139
+ pcp_rank=self.pcp_rank,
1140
+ dcp_rank=self.dcp_rank,
1141
+ pp_rank=self.pp_rank,
1142
+ cache_prefix=str(
1143
+ vllm_config.kv_transfer_config.kv_connector_extra_config.get(
1144
+ "cache_prefix", ""
1145
+ )
1146
+ ),
1147
+ )
1148
+ self._group_tp_replication_factors: tuple[int, ...] = (
1149
+ self._compute_group_tp_replication_factors()
1150
+ )
1151
self.token_dbs: list[ChunkedTokenDatabase] = [
1152
ChunkedTokenDatabase(
1162
- dataclasses.replace(self.metadata, group_id=g_idx),
1153
+ dataclasses.replace(
1154
+ metadata,
1155
+ group_id=g_idx,
1156
+ tp_rank=self.tp_rank // self._group_tp_replication_factors[g_idx],
1157
+ ),
1158
g.kv_cache_spec.block_size,
1159
hash_block_size=self.hash_block_size,
1160
)
@@ -1167,27 +1162,50 @@ class MooncakeStoreWorker:
1162
]
1163
self._init_lookup_key_prefixes()
1164
1170
- def _init_lookup_key_prefixes(self) -> None:
1171
- """Prepare per-group key prefixes across parallel rank namespaces."""
1172
- # (tp_rank, pcp_rank, dcp_rank, pp_rank) namespaces
1165
+ def _spec_tp_replication_factor(self, spec: KVCacheSpec) -> int:
1166
if self.dcp_size > 1:
1174
- # DCP reuses the TP workers and splits each TP group into
1175
- # contiguous DCP groups, so dcp_rank == tp_rank % dcp_size.
1176
- # Store/load paths do not apply KV-head dedup under DCP
1177
- rank_namespaces = tuple(
1178
- (tp_rank, pcp_rank, tp_rank % self.dcp_size, pp_rank)
1179
- for pcp_rank in range(self.pcp_size)
1180
- for tp_rank in range(self.tp_size)
1181
- for pp_rank in range(self.pp_size)
1182
- )
1183
- else:
1184
- # Without DCP, TP ranks that share a KV head write identical KV, so
1185
- # lookup only needs one TP namespace per unique KV head.
1186
- tp_count = min(self.tp_size, self.num_kv_head)
1187
- rank_namespaces = tuple(
1188
- (tp_rank, pcp_rank, 0, pp_rank)
1167
+ return 1
1168
+ inner_specs = (
1169
+ tuple(spec.kv_cache_specs.values())
1170
+ if isinstance(spec, UniformTypeKVCacheSpecs)
1171
+ else (spec,)
1172
+ )
1173
+ # Any rank-specific state makes the whole packed value rank-specific.
1174
+ if any(isinstance(inner, MambaSpec) for inner in inner_specs):
1175
+ return 1
1176
+ # A pure MLA packed value is replicated on every TP rank.
1177
+ if all(
1178
+ isinstance(inner, (MLAAttentionSpec, SlidingWindowMLASpec))
1179
+ for inner in inner_specs
1180
+ ):
1181
+ return self.tp_size
1182
+ return max(1, self.tp_size // self.num_kv_head)
1183
+
1184
+ def _compute_group_tp_replication_factors(self) -> tuple[int, ...]:
1185
+ """Return the number of byte-identical TP replicas per cache group.
1186
+
1187
+ DCP and Mamba use 1; MLA uses ``tp_size``; GQA uses
1188
+ ``tp_size // num_kv_head``.
1189
+ """
1190
+ return tuple(
1191
+ self._spec_tp_replication_factor(group.kv_cache_spec)
1192
+ for group in self._kv_cache_groups
1193
+ )
1194
+
1195
+ def _init_lookup_key_prefixes(self) -> None:
1196
+ def rank_namespaces(factor: int) -> tuple[tuple[int, int, int, int], ...]:
1197
+ if self.dcp_size > 1:
1198
+ # DCP is a TP subdivision: dcp_rank == tp_rank % dcp_size.
1199
+ return tuple(
1200
+ (tp_rank, pcp_rank, tp_rank % self.dcp_size, pp_rank)
1201
+ for pcp_rank in range(self.pcp_size)
1202
+ for tp_rank in range(self.tp_size)
1203
+ for pp_rank in range(self.pp_size)
1204
+ )
1205
+ return tuple(
1206
+ (shard_rank, pcp_rank, 0, pp_rank)
1207
for pcp_rank in range(self.pcp_size)
1190
- for tp_rank in range(tp_count)
1208
+ for shard_rank in range(self.tp_size // factor)
1209
for pp_rank in range(self.pp_size)
1210
)
1211
@@ -1200,11 +1218,12 @@ class MooncakeStoreWorker:
1218
dcp_rank=dcp_rank,
1219
pp_rank=pp_rank,
1220
)
1203
- for tp_rank, pcp_rank, dcp_rank, pp_rank in rank_namespaces
1221
+ for tp_rank, pcp_rank, dcp_rank, pp_rank in rank_namespaces(
1222
+ self._group_tp_replication_factors[g_idx]
1223
+ )
1224
)
1205
- for db in self.token_dbs
1225
+ for g_idx, db in enumerate(self.token_dbs)
1226
)
1207
- self._lookup_expected_per_key = len(rank_namespaces)
1227
1228
def register_cross_layers_kv_caches(self, kv_cache: torch.Tensor) -> None:
1229
"""Register a cross-layers KV cache tensor.
@@ -1297,7 +1316,7 @@ class MooncakeStoreWorker:
1316
self.token_dbs,
1317
self.block_size,
1318
self.tp_rank,
1300
- self.put_step,
1319
+ self._group_tp_replication_factors,
1320
self.kv_role,
1321
ready_event_sending,
1322
self.enable_kv_events,
@@ -1527,16 +1546,16 @@ class MooncakeStoreWorker:
1546
logger.error("Remote connection failed in lookup: %s", e)
1547
return 0
1548
1530
- # A (group, hash) is "present" only when every TP*PP rank has it.
1531
- ranks_per_candidate = self._lookup_expected_per_key
1532
- exists_set = {
1533
- (g_idx, hash_bytes)
1534
- for i, (g_idx, hash_bytes) in enumerate(candidate_meta)
1535
- if all(
1536
- res[i * ranks_per_candidate + j] == 1
1537
- for j in range(ranks_per_candidate)
1538
- )
1539
- }
1549
+ # A (group, hash) is "present" only when every namespace that will be
1550
+ # loaded has it (per-group count: sharded groups need every rank's
1551
+ # shard, replicated groups one namespace per unique KV head).
1552
+ exists_set = set()
1553
+ pos = 0
1554
+ for g_idx, hash_bytes in candidate_meta:
1555
+ count = len(self._lookup_key_prefixes[g_idx])
1556
+ if all(res[pos + j] == 1 for j in range(count)):
1557
+ exists_set.add((g_idx, hash_bytes))
1558
+ pos += count
1559
1560
cached_block_pool = ExternalCachedBlockPool(
1561
self.hash_block_size,