[KV Connector][Mooncake] Add tenant ID support to MooncakeStoreConnector (#48069)
Signed-off-by: lzw <2994587066@qq.com> Co-authored-by: Roger Wang <hey@rogerw.io>
LZW committed
Aug 6, 2026 at 08:31 UTC
d36f24b66a9ce71cf15f1cb9249f5d4092f32d11
3 files changed
+149
-2
docs/features/mooncake_store_connector_usage.md
+23
@@ -61,6 +61,9 @@ Create a JSON configuration file (e.g., `mooncake_config.json`):
61
large prefills do not exceed the owner's SSD-write budget. Set this together
62
with the matching `--enable_offload=true` flag on `mooncake_master` and on
63
the external `mooncake_client` (if any).
64
+- `tenant_id`: Optional Mooncake tenant namespace. Producers and consumers
65
+ that should share store data must use the same tenant id. Default:
66
+ `"default"`.
67
68
Set the config path via environment variable:
69
@@ -181,6 +184,26 @@ Mooncake environment variables (`MOONCAKE_OFFLOAD_FILE_STORAGE_PATH`,
184
`MOONCAKE_OFFLOAD_TOTAL_SIZE_LIMIT_BYTES`, etc.). Those are independent of
185
the vLLM JSON config.
186
187
+### Tenant Isolation
188
+
189
+Set `tenant_id` in the Mooncake JSON config when different vLLM deployments should use separate Mooncake tenant namespaces:
190
+
191
+```json
192
+{
193
+ "mode": "embedded",
194
+ "metadata_server": "P2PHANDSHAKE",
195
+ "master_server_address": "127.0.0.1:50051",
196
+ "global_segment_size": "80GB",
197
+ "local_buffer_size": "4GB",
198
+ "protocol": "rdma",
199
+ "device_name": "",
200
+ "enable_offload": false,
201
+ "tenant_id": "tenant-a"
202
+}
203
+```
204
+
205
+Strict isolation requires a Mooncake master started with `--enable_multi_tenants=true` and a tenant quota policy that registers each tenant. Non-default `tenant_id` also requires a Mooncake version whose `MooncakeDistributedStore.setup()` accepts the `tenant_id` parameter. In `standalone-store` mode, start the external `mooncake_client` with the matching tenant id because that process owns the real store client.
206
+
207
## Environment Variables
208
209
| Variable | Description | Default |
tests/v1/kv_connector/unit/test_mooncake_store_worker.py
+106
-1
@@ -1599,7 +1599,7 @@ def test_requester_worker_init_prefers_local_hostname_override(
1599
},
1600
),
1601
)
1602
- worker.MooncakeStoreWorker(_make_vllm_config(), _make_kv_cache_config())
1602
+ worker.MooncakeStoreWorker(_make_vllm_config(rank=1), _make_kv_cache_config())
1603
1604
assert store.setup.call_args.args[0] == "worker-a:50053"
1605
@@ -2857,6 +2857,7 @@ def test_config_defaults_to_embedded():
2857
assert cfg.global_segment_size == worker.DEFAULT_GLOBAL_SEGMENT_SIZE
2858
assert cfg.local_buffer_size == worker.DEFAULT_LOCAL_BUFFER_SIZE
2859
assert cfg.enable_offload is False
2860
+ assert cfg.tenant_id == worker.DEFAULT_TENANT_ID
2861
2862
2863
def test_config_pr40900_unchanged(tmp_path):
@@ -2878,6 +2879,51 @@ def test_config_pr40900_unchanged(tmp_path):
2879
assert cfg.global_segment_size == 4 * 1024**3
2880
assert cfg.local_buffer_size == 4 * 1024**3
2881
assert cfg.enable_offload is False
2882
+ assert cfg.tenant_id == worker.DEFAULT_TENANT_ID
2883
+
2884
+
2885
+def test_config_from_file_normalizes_tenant_id(tmp_path):
2886
+ config_path = _write_mooncake_config(
2887
+ tmp_path,
2888
+ {
2889
+ "metadata_server": "http://metadata/endpoint",
2890
+ "master_server_address": "10.0.0.7:50051",
2891
+ "tenant_id": " tenant-a ",
2892
+ },
2893
+ )
2894
+
2895
+ cfg = worker.MooncakeStoreConfig.from_file(config_path)
2896
+
2897
+ assert cfg.tenant_id == "tenant-a"
2898
+
2899
+
2900
+def test_config_from_file_normalizes_empty_tenant_id_to_default(tmp_path):
2901
+ config_path = _write_mooncake_config(
2902
+ tmp_path,
2903
+ {
2904
+ "metadata_server": "http://metadata/endpoint",
2905
+ "master_server_address": "10.0.0.7:50051",
2906
+ "tenant_id": " ",
2907
+ },
2908
+ )
2909
+
2910
+ cfg = worker.MooncakeStoreConfig.from_file(config_path)
2911
+
2912
+ assert cfg.tenant_id == worker.DEFAULT_TENANT_ID
2913
+
2914
+
2915
+def test_config_from_file_rejects_non_string_tenant_id(tmp_path):
2916
+ config_path = _write_mooncake_config(
2917
+ tmp_path,
2918
+ {
2919
+ "metadata_server": "http://metadata/endpoint",
2920
+ "master_server_address": "10.0.0.7:50051",
2921
+ "tenant_id": False,
2922
+ },
2923
+ )
2924
+
2925
+ with pytest.raises(TypeError, match="tenant_id must be a string or null"):
2926
+ worker.MooncakeStoreConfig.from_file(config_path)
2927
2928
2929
def test_config_embedded_rejects_zero_segment():
@@ -2961,6 +3007,7 @@ def test_topology_standalone_store_with_disk_offload(tmp_path, monkeypatch):
3007
"mlx5_0",
3008
"10.0.0.7:50051",
3009
)
3010
+ assert store.setup.call_args.kwargs == {}
3011
# ReplicateConfig is built and carries the preferred_segment.
3012
assert isinstance(w.store_replicate_config, fake_replicate_config_cls)
3013
assert w.store_replicate_config.preferred_segment == "10.0.0.7:50053"
@@ -3004,6 +3051,7 @@ def test_topology_embedded_cpu_only(tmp_path, monkeypatch):
3051
"mlx5_0",
3052
"10.0.0.7:50051",
3053
)
3054
+ assert store.setup.call_args.kwargs == {}
3055
# No preferred_segment — ReplicateConfig is default-constructed (so the
3056
# preferred_segment field keeps its default value).
3057
assert w.preferred_segment is None
@@ -3013,6 +3061,63 @@ def test_topology_embedded_cpu_only(tmp_path, monkeypatch):
3061
assert w.disk_offload_buffer_budget_bytes is None
3062
3063
3064
+def test_topology_forwards_non_default_tenant_id(tmp_path, monkeypatch):
3065
+ store = MagicMock()
3066
+ store.setup.return_value = 0
3067
+ _install_fake_mooncake(monkeypatch, store)
3068
+ _patch_worker_runtime(monkeypatch)
3069
+ monkeypatch.setenv(
3070
+ "MOONCAKE_CONFIG_PATH",
3071
+ _write_mooncake_config(
3072
+ tmp_path,
3073
+ {
3074
+ "metadata_server": "http://metadata/endpoint",
3075
+ "global_segment_size": "4GB",
3076
+ "local_buffer_size": "4GB",
3077
+ "protocol": "rdma",
3078
+ "device_name": "mlx5_0",
3079
+ "master_server_address": "10.0.0.7:50051",
3080
+ "tenant_id": " tenant-a ",
3081
+ },
3082
+ ),
3083
+ )
3084
+
3085
+ worker.MooncakeStoreWorker(_make_vllm_config(rank=1), _make_kv_cache_config())
3086
+
3087
+ assert store.setup.call_args.kwargs == {"tenant_id": "tenant-a"}
3088
+
3089
+
3090
+def test_non_default_tenant_preserves_setup_type_error(tmp_path, monkeypatch):
3091
+ store = MagicMock()
3092
+ setup_error = TypeError(
3093
+ "setup(): incompatible function arguments; "
3094
+ "supported signature includes tenant_id"
3095
+ )
3096
+ store.setup.side_effect = setup_error
3097
+ _install_fake_mooncake(monkeypatch, store)
3098
+ _patch_worker_runtime(monkeypatch)
3099
+ monkeypatch.setenv(
3100
+ "MOONCAKE_CONFIG_PATH",
3101
+ _write_mooncake_config(
3102
+ tmp_path,
3103
+ {
3104
+ "metadata_server": "http://metadata/endpoint",
3105
+ "global_segment_size": "4GB",
3106
+ "local_buffer_size": "4GB",
3107
+ "protocol": "rdma",
3108
+ "device_name": "mlx5_0",
3109
+ "master_server_address": "10.0.0.7:50051",
3110
+ "tenant_id": "tenant-a",
3111
+ },
3112
+ ),
3113
+ )
3114
+
3115
+ with pytest.raises(TypeError) as exc_info:
3116
+ worker.MooncakeStoreWorker(_make_vllm_config(rank=1), _make_kv_cache_config())
3117
+
3118
+ assert exc_info.value is setup_error
3119
+
3120
+
3121
# ---------------------------------------------------------------------------
3122
# Stats/metrics tests (PR-35 port)
3123
# ---------------------------------------------------------------------------
vllm/distributed/kv_transfer/kv_connector/v1/mooncake/store/worker.py
+20
-1
@@ -80,6 +80,7 @@ logger = init_logger(__name__)
80
81
DEFAULT_GLOBAL_SEGMENT_SIZE = 4 * 1024 * 1024 * 1024 # 4 GiB
82
DEFAULT_LOCAL_BUFFER_SIZE = 4 * 1024 * 1024 * 1024 # 4 GiB
83
+DEFAULT_TENANT_ID = "default"
84
85
MOONCAKE_NO_AVAILABLE_HANDLE = -200
86
_T = TypeVar("_T")
@@ -134,6 +135,7 @@ class MooncakeStoreConfig:
135
global_segment_size: int = DEFAULT_GLOBAL_SEGMENT_SIZE
136
local_buffer_size: int = DEFAULT_LOCAL_BUFFER_SIZE
137
enable_offload: bool = False
138
+ tenant_id: str = DEFAULT_TENANT_ID
139
140
def __post_init__(self) -> None:
141
if self.mode not in ("embedded", "standalone-store"):
@@ -162,6 +164,7 @@ class MooncakeStoreConfig:
164
config.get("local_buffer_size", DEFAULT_LOCAL_BUFFER_SIZE)
165
),
166
enable_offload=bool(config.get("enable_offload", False)),
167
+ tenant_id=_normalize_tenant_id(config.get("tenant_id", DEFAULT_TENANT_ID)),
168
)
169
170
@staticmethod
@@ -174,6 +177,17 @@ class MooncakeStoreConfig:
177
return MooncakeStoreConfig.from_file(config_path)
178
179
180
+def _normalize_tenant_id(value: Any) -> str:
181
+ if value is None:
182
+ return DEFAULT_TENANT_ID
183
+ if not isinstance(value, str):
184
+ raise TypeError(
185
+ f"tenant_id must be a string or null, got {type(value).__name__}: {value!r}"
186
+ )
187
+ tenant_id = value.strip()
188
+ return tenant_id if tenant_id else DEFAULT_TENANT_ID
189
+
190
+
191
def _parse_size(value: Any) -> int:
192
"""Parse storage size strings with units: GB, MB, KB, B."""
193
if isinstance(value, int):
@@ -1245,6 +1259,9 @@ class MooncakeStoreWorker:
1259
self.store = MooncakeDistributedStore()
1260
local_ip = get_ip()
1261
local_hostname = rdma_utils.get_requester_local_hostname(local_ip)
1262
+ setup_kwargs: dict[str, str] = {}
1263
+ if store_config.tenant_id != DEFAULT_TENANT_ID:
1264
+ setup_kwargs["tenant_id"] = store_config.tenant_id
1265
ret = self.store.setup(
1266
local_hostname,
1267
store_config.metadata_server,
@@ -1253,6 +1270,7 @@ class MooncakeStoreWorker:
1270
store_config.protocol,
1271
store_config.device_name,
1272
store_config.master_server_address,
1273
+ **setup_kwargs,
1274
)
1275
if ret != 0:
1276
msg = "Initialize MooncakeDistributedStore failed."
@@ -1280,12 +1298,13 @@ class MooncakeStoreWorker:
1298
1299
logger.info(
1300
"Mooncake mode=%s (global_segment_size=%d, local_buffer_size=%d, "
1283
- "preferred_segment=%s, enable_offload=%s)",
1301
+ "preferred_segment=%s, enable_offload=%s, tenant_id=%s)",
1302
store_config.mode,
1303
store_config.global_segment_size,
1304
store_config.local_buffer_size,
1305
preferred_segment or "<none>",
1306
store_config.enable_offload,
1307
+ store_config.tenant_id,
1308
)
1309
if store_config.mode == "embedded":
1310
if store_config.enable_offload and preferred_segment is None: