[mooncake] support CUSTOM_MEM_POOL in vllm (#49300)
Signed-off-by: bruce.xu <bruce.xb@alibaba-inc.com> Signed-off-by: Yifan Qiao <yifanqiao@inferact.ai> Co-authored-by: bruce.xu <bruce.xb@alibaba-inc.com> Co-authored-by: Yifan Qiao <yifanqiao@inferact.ai> Co-authored-by: OpenAI Codex <codex@openai.com>
void committed
Sep 27, 2026 at 08:26 UTC
eb0f2ca37f65e496e8c67a7497e818c8a92bae46
7 files changed
+193
-4
tests/v1/kv_connector/unit/test_mooncake_store_worker.py
+60
@@ -290,6 +290,8 @@ class _FakeKVTransferConfig:
290
class _FakeModelConfig:
291
model = "test-model"
292
use_mla = False
293
+ enable_sleep_mode = False
294
+ enable_cumem_allocator = False
295
296
def get_num_layers(self, parallel_config) -> int:
297
return 1
@@ -432,6 +434,64 @@ def _patch_worker_runtime(
434
monkeypatch.setattr(worker, "LookupKeyServer", MagicMock())
435
436
437
+def test_create_mem_pool_returns_none_when_unconfigured():
438
+ assert worker.MooncakeStoreWorker._create_mem_pool({}, _FakeModelConfig()) is None
439
+
440
+
441
+def test_create_mem_pool_rejects_unknown_pool():
442
+ with pytest.raises(
443
+ ValueError,
444
+ match="Unsupported custom_mem_pool='UNKNOWN'",
445
+ ):
446
+ worker.MooncakeStoreWorker._create_mem_pool(
447
+ {"custom_mem_pool": "unknown"}, _FakeModelConfig()
448
+ )
449
+
450
+
451
+@pytest.mark.parametrize(
452
+ "conflicting_option", ["enable_sleep_mode", "enable_cumem_allocator"]
453
+)
454
+def test_create_mem_pool_rejects_cumem_conflicts(conflicting_option):
455
+ model_config = _FakeModelConfig()
456
+ setattr(model_config, conflicting_option, True)
457
+
458
+ with pytest.raises(ValueError, match="custom_mem_pool is incompatible"):
459
+ worker.MooncakeStoreWorker._create_mem_pool(
460
+ {"custom_mem_pool": "NVLINK"}, model_config
461
+ )
462
+
463
+
464
+@pytest.mark.parametrize(
465
+ ("pool_type", "allocator_name"),
466
+ [("nvlink", "NVLinkAllocator"), ("barex", "BarexAllocator")],
467
+)
468
+def test_create_mem_pool_uses_mooncake_allocator(
469
+ monkeypatch, pool_type, allocator_name
470
+):
471
+ allocator_cls = MagicMock()
472
+ allocator = allocator_cls.get_allocator.return_value
473
+ allocator_module = types.ModuleType("mooncake.allocator")
474
+ setattr(allocator_module, allocator_name, allocator_cls)
475
+ mooncake_module = types.ModuleType("mooncake")
476
+ mooncake_module.allocator = allocator_module # type: ignore[attr-defined]
477
+ monkeypatch.setitem(sys.modules, "mooncake", mooncake_module)
478
+ monkeypatch.setitem(sys.modules, "mooncake.allocator", allocator_module)
479
+
480
+ monkeypatch.setattr(torch.accelerator, "current_device_index", lambda: 2)
481
+ mem_pool = MagicMock()
482
+ mem_pool_ctor = MagicMock(return_value=mem_pool)
483
+ monkeypatch.setattr(torch.cuda, "MemPool", mem_pool_ctor)
484
+
485
+ result = worker.MooncakeStoreWorker._create_mem_pool(
486
+ {"custom_mem_pool": pool_type}, _FakeModelConfig()
487
+ )
488
+
489
+ assert result is mem_pool
490
+ allocator_cls.get_allocator.assert_called_once_with(torch.device("cuda", 2))
491
+ allocator.allocator.assert_called_once_with()
492
+ mem_pool_ctor.assert_called_once_with(allocator.allocator.return_value)
493
+
494
+
495
def test_pool_key_to_string_without_prefix_is_unchanged():
496
"""Default (empty) cache_prefix keeps keys byte-identical to the
497
historical unprefixed format so existing deployments keep their hits."""
tests/v1/kv_connector/unit/test_multi_connector.py
+32
@@ -68,6 +68,7 @@ class MockConnector(KVConnectorBase_V1):
68
# Override just build_kv_connector_stats
69
mock.build_kv_connector_stats = cls.build_kv_connector_stats
70
mock.get_kv_connector_stats.return_value = None
71
+ mock.get_mem_pool_context.return_value = None
72
return mock
73
74
@classmethod
@@ -201,6 +202,36 @@ def mc() -> MultiConnector:
202
return mc
203
204
205
+def test_multi_connector_mem_pool_context_none(mc: MultiConnector):
206
+ assert mc.get_mem_pool_context() is None
207
+
208
+
209
+def test_multi_connector_forwards_mem_pool_context(mc: MultiConnector):
210
+ context = MagicMock()
211
+ provider = MagicMock(spec_set=KVConnectorBase_V1)
212
+ provider.get_mem_pool_context.return_value = context
213
+ mc._connectors = [mc._connectors[0], provider]
214
+
215
+ assert mc.get_mem_pool_context() is context
216
+ provider.get_mem_pool_context.assert_called_once_with()
217
+
218
+
219
+def test_multi_connector_rejects_multiple_mem_pool_contexts(mc: MultiConnector):
220
+ providers = [
221
+ MagicMock(spec_set=KVConnectorBase_V1),
222
+ MagicMock(spec_set=KVConnectorBase_V1),
223
+ ]
224
+ for provider in providers:
225
+ provider.get_mem_pool_context.return_value = MagicMock()
226
+ mc._connectors = providers
227
+
228
+ with pytest.raises(
229
+ ValueError,
230
+ match="Multiple connectors provide a KV cache memory pool",
231
+ ):
232
+ mc.get_mem_pool_context()
233
+
234
+
235
# Helper function to compare directories recursively
236
def _compare_directories(dir1: Path, dir2: Path) -> bool:
237
"""Compares two directories recursively for identical content."""
@@ -320,6 +351,7 @@ def test_multi_example_connector_consistency():
351
# First three events are from initialization. Layer hooks run before the
352
# deferred load starts after the forward pass.
353
expected_worker_prefix = [
354
+ "get_mem_pool_context",
355
"register_kv_caches",
356
"set_host_xfer_buffer_ops",
357
"get_handshake_metadata",
vllm/distributed/kv_transfer/kv_connector/v1/base.py
+8
@@ -42,6 +42,7 @@ The class provides the following primitives:
42
import enum
43
from abc import ABC, abstractmethod
44
from collections.abc import Callable, Iterable
45
+from contextlib import AbstractContextManager
46
from dataclasses import dataclass, field
47
from typing import TYPE_CHECKING, Any, Literal
48
@@ -227,6 +228,13 @@ class KVConnectorBase_V1(ABC):
228
# Worker-side methods
229
# ==============================
230
231
+ def get_mem_pool_context(self) -> AbstractContextManager | None:
232
+ """Return a custom KV cache allocation context, if configured.
233
+
234
+ Returning None uses the engine's default memory pool.
235
+ """
236
+ return None
237
+
238
def bind_connector_metadata(self, connector_metadata: KVConnectorMetadata) -> None:
239
"""Set the connector metadata from the scheduler.
240
vllm/distributed/kv_transfer/kv_connector/v1/mooncake/store/connector.py
+12
@@ -13,6 +13,7 @@ enabling prefix caching via hash-based deduplication.
13
14
from collections import Counter
15
from collections.abc import Iterable, Sequence
16
+from contextlib import AbstractContextManager
17
from typing import Any
18
19
import torch
@@ -322,6 +323,17 @@ class MooncakeStoreConnector(KVConnectorBase_V1, SupportsHMA):
323
# Worker-side methods
324
# ============================================================
325
326
+ def get_mem_pool_context(self) -> AbstractContextManager | None:
327
+ """Return a context manager for the custom MemPool, or None.
328
+
329
+ Called by the Worker before ``initialize_kv_cache`` so that KV
330
+ cache is allocated from the Mooncake-managed pool when
331
+ ``custom_mem_pool`` is set in ``kv_connector_extra_config``.
332
+ """
333
+ if self.connector_worker is None:
334
+ return None
335
+ return self.connector_worker.get_mem_pool_context()
336
+
337
def register_kv_caches(self, kv_caches: dict[str, torch.Tensor]):
338
assert self.connector_worker is not None
339
self.connector_worker.register_kv_caches(kv_caches)
vllm/distributed/kv_transfer/kv_connector/v1/mooncake/store/worker.py
+48
-1
@@ -19,13 +19,14 @@ import threading
19
import time
20
from collections.abc import Callable, Sequence
21
from concurrent.futures import Future, ThreadPoolExecutor
22
+from contextlib import AbstractContextManager
23
from typing import Any, TypeVar
24
25
import torch
26
import zmq
27
28
import vllm.envs as envs
28
-from vllm.config import VllmConfig
29
+from vllm.config import ModelConfig, VllmConfig
30
from vllm.distributed import (
31
get_dcp_group,
32
get_pcp_group,
@@ -1414,6 +1415,41 @@ class KVCacheStoreRecvingThread(KVTransferThread):
1415
class MooncakeStoreWorker:
1416
"""Worker-side component for MooncakeStoreConnector."""
1417
1418
+ @staticmethod
1419
+ def _create_mem_pool(
1420
+ extra_config: dict[str, Any], model_config: ModelConfig
1421
+ ) -> torch.cuda.MemPool | None:
1422
+ pool_type = str(extra_config.get("custom_mem_pool") or "").upper()
1423
+ if not pool_type:
1424
+ return None
1425
+ if pool_type not in ("NVLINK", "BAREX"):
1426
+ raise ValueError(
1427
+ f"Unsupported custom_mem_pool={pool_type!r}, "
1428
+ "expected 'NVLINK' or 'BAREX'"
1429
+ )
1430
+ if model_config.enable_sleep_mode or model_config.enable_cumem_allocator:
1431
+ raise ValueError(
1432
+ "custom_mem_pool is incompatible with enable_sleep_mode "
1433
+ "or enable_cumem_allocator; CuMemAllocator cannot manage "
1434
+ "allocations from the custom pool."
1435
+ )
1436
+ try:
1437
+ if pool_type == "NVLINK":
1438
+ from mooncake.allocator import NVLinkAllocator as allocator_cls
1439
+ else: # pool_type == "BAREX"
1440
+ from mooncake.allocator import BarexAllocator as allocator_cls
1441
+ except ImportError as e:
1442
+ raise ImportError(
1443
+ f"custom_mem_pool={pool_type!r} requires "
1444
+ "mooncake-transfer-engine>=0.3.8. Please upgrade Mooncake."
1445
+ ) from e
1446
+
1447
+ device = torch.device("cuda", torch.accelerator.current_device_index())
1448
+ allocator = allocator_cls.get_allocator(device)
1449
+ mem_pool = torch.cuda.MemPool(allocator.allocator())
1450
+ logger.info("Using Mooncake custom memory pool: %s", pool_type)
1451
+ return mem_pool
1452
+
1453
def __init__(
1454
self,
1455
vllm_config: VllmConfig,
@@ -1471,6 +1507,10 @@ class MooncakeStoreWorker:
1507
1508
# Initialize MooncakeDistributedStore with its own TransferEngine
1509
store_config = MooncakeStoreConfig.load_from_config()
1510
+ # Validate and create the custom memory pool before initializing the
1511
+ # store so failures cannot leak a partially initialized store handle.
1512
+ self._mem_pool = self._create_mem_pool(extra_config, model_config)
1513
+
1514
self.store = MooncakeDistributedStore()
1515
local_ip = get_ip()
1516
local_hostname = rdma_utils.get_requester_local_hostname(local_ip)
@@ -1741,6 +1781,13 @@ class MooncakeStoreWorker:
1781
)
1782
return token_dbs
1783
1784
+ def get_mem_pool_context(self) -> AbstractContextManager | None:
1785
+ """Return a context manager for the custom MemPool, or None if
1786
+ no custom pool is configured."""
1787
+ if self._mem_pool is None:
1788
+ return None
1789
+ return torch.cuda.use_mem_pool(self._mem_pool)
1790
+
1791
def _spec_tp_replication_factor(self, spec: KVCacheSpec) -> int:
1792
if self.dcp_size > 1:
1793
return 1
vllm/distributed/kv_transfer/kv_connector/v1/multi_connector.py
+21
@@ -2,6 +2,7 @@
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
import copy
4
from collections.abc import Callable, Iterable
5
+from contextlib import AbstractContextManager
6
from dataclasses import dataclass
7
from typing import TYPE_CHECKING, Any, cast
8
@@ -294,6 +295,26 @@ class MultiConnector(KVConnectorBase_V1, SupportsHMA):
295
# ==============================
296
# Worker-side methods
297
# ==============================
298
+ def get_mem_pool_context(self) -> AbstractContextManager | None:
299
+ """Forward a custom KV cache memory pool from a child connector.
300
+
301
+ KV cache is allocated once and can only live in one pool, so at
302
+ most one child may provide a context.
303
+ """
304
+ found: list[tuple[str, AbstractContextManager]] = []
305
+ for connector in self._connectors:
306
+ if (ctx := connector.get_mem_pool_context()) is not None:
307
+ found.append((type(connector).__name__, ctx))
308
+
309
+ if len(found) > 1:
310
+ names = [name for name, _ in found]
311
+ raise ValueError(
312
+ f"Multiple connectors provide a KV cache memory pool {names}; "
313
+ "KV cache is allocated once and can only live in one pool. "
314
+ "Configure custom_mem_pool on at most one connector."
315
+ )
316
+ return found[0][1] if found else None
317
+
318
def start_load_kv(self, forward_context: "ForwardContext", **kwargs) -> None:
319
for c in self._connectors:
320
c.start_load_kv(forward_context, **kwargs)
vllm/v1/worker/gpu_worker.py
+12
-3
@@ -801,11 +801,20 @@ class Worker(WorkerBase):
801
# related to kv cache connector (e.g. kv cache sharing layers).
802
ensure_kv_transfer_initialized(self.vllm_config, kv_cache_config)
803
804
+ # If the connector provides a custom memory pool (e.g. Mooncake
805
+ # NVLink/BAREX), use it for KV cache allocation; otherwise fall
806
+ # back to the standard CuMem pool.
807
+ mem_pool_context = (
808
+ get_kv_transfer_group().get_mem_pool_context()
809
+ if has_kv_transfer_group()
810
+ else None
811
+ )
812
+ if mem_pool_context is None:
813
+ mem_pool_context = self._maybe_get_memory_pool_context(tag="kv_cache")
814
+
815
self.model_runner.initialize_kv_cache(
816
kv_cache_config,
806
- kv_cache_allocation_context=self._maybe_get_memory_pool_context(
807
- tag="kv_cache"
808
- ),
817
+ kv_cache_allocation_context=mem_pool_context,
818
)
819
820
# Build KV-zero metadata outside the CuMem pool so the bookkeeping