diff --git a/python/sglang/srt/mem_cache/storage/mooncake_store/README.md b/python/sglang/srt/mem_cache/storage/mooncake_store/README.md index b9078f387..52860a90e 100644 --- a/python/sglang/srt/mem_cache/storage/mooncake_store/README.md +++ b/python/sglang/srt/mem_cache/storage/mooncake_store/README.md @@ -196,6 +196,10 @@ Mooncake loads configuration in the following priority order: 2. If not, Mooncake checks whether the environment variable `DEFAULT_MOONCAKE_CONFIG_PATH_ENV` is set, and loads the JSON config file from that path. 3. If neither of the above is provided, Mooncake falls back to environment variables. +For multi-node deployments that attach Mooncake at runtime via `PUT /hicache/storage-backend`, omit `local_hostname` from the attach payload and set `MOONCAKE_LOCAL_HOSTNAME` (or `LOCAL_HOSTNAME`) per node before launching SGLang. Each rank resolves `local_hostname` from its own process environment instead of a shared default. + +When loading from a JSON config file, `local_hostname` follows the same per-process precedence: `MOONCAKE_LOCAL_HOSTNAME`, then `LOCAL_HOSTNAME`, then the value in the JSON file, then `"localhost"`. + **Using extra-config of sglang arguments to configure Mooncake** ```bash diff --git a/python/sglang/srt/mem_cache/storage/mooncake_store/mooncake_store.py b/python/sglang/srt/mem_cache/storage/mooncake_store/mooncake_store.py index 45c1e9023..2146298d0 100644 --- a/python/sglang/srt/mem_cache/storage/mooncake_store/mooncake_store.py +++ b/python/sglang/srt/mem_cache/storage/mooncake_store/mooncake_store.py @@ -106,6 +106,25 @@ class MooncakeStoreConfig: ssd_offload_path: Optional[str] = None tenant_id: str = DEFAULT_TENANT_ID + @staticmethod + def _resolve_local_hostname(overrides: Optional[dict] = None) -> str: + """Resolve local_hostname for the current process. + + Process environment takes precedence over config overrides so multi-node + runtime attach can broadcast shared extra_config while each node uses its + own MOONCAKE_LOCAL_HOSTNAME / LOCAL_HOSTNAME. + """ + if envs.MOONCAKE_LOCAL_HOSTNAME.is_set(): + return envs.MOONCAKE_LOCAL_HOSTNAME.get() + local_hostname = os.getenv("LOCAL_HOSTNAME") + if local_hostname: + return local_hostname + if overrides is not None: + value = overrides.get("local_hostname") + if value: + return value + return envs.MOONCAKE_LOCAL_HOSTNAME.default + @staticmethod def from_file() -> "MooncakeStoreConfig": """Load the config from a JSON file.""" @@ -129,9 +148,7 @@ class MooncakeStoreConfig: ) return MooncakeStoreConfig( - local_hostname=config.get( - "local_hostname", envs.MOONCAKE_LOCAL_HOSTNAME.default - ), + local_hostname=MooncakeStoreConfig._resolve_local_hostname(config), metadata_server=config.get( "metadata_server", envs.MOONCAKE_TE_META_DATA_SERVER.default ), @@ -180,18 +197,8 @@ class MooncakeStoreConfig: "Either the environment variable 'MOONCAKE_MASTER' or 'MOONCAKE_CLIENT' is not set." ) - # Special handling for local_hostname: try MOONCAKE_LOCAL_HOSTNAME first, - # then fall back to LOCAL_HOSTNAME if not set. - # This is for forward compatibility with the legacy LOCAL_HOSTNAME environment variable. - if envs.MOONCAKE_LOCAL_HOSTNAME.is_set(): - local_hostname = envs.MOONCAKE_LOCAL_HOSTNAME.get() - else: - local_hostname = os.getenv( - "LOCAL_HOSTNAME", envs.MOONCAKE_LOCAL_HOSTNAME.default - ) - return MooncakeStoreConfig( - local_hostname=local_hostname, + local_hostname=MooncakeStoreConfig._resolve_local_hostname(), metadata_server=envs.MOONCAKE_TE_META_DATA_SERVER.get(), global_segment_size=_parse_global_segment_size( envs.MOONCAKE_GLOBAL_SEGMENT_SIZE.get() @@ -220,9 +227,7 @@ class MooncakeStoreConfig: ) return MooncakeStoreConfig( - local_hostname=extra_config.get( - "local_hostname", envs.MOONCAKE_LOCAL_HOSTNAME.default - ), + local_hostname=MooncakeStoreConfig._resolve_local_hostname(extra_config), metadata_server=extra_config.get( "metadata_server", envs.MOONCAKE_TE_META_DATA_SERVER.default ), diff --git a/test/registered/unit/mem_cache/test_mooncake_store_config.py b/test/registered/unit/mem_cache/test_mooncake_store_config.py new file mode 100644 index 000000000..6b724cf82 --- /dev/null +++ b/test/registered/unit/mem_cache/test_mooncake_store_config.py @@ -0,0 +1,122 @@ +"""Unit tests for MooncakeStoreConfig local_hostname resolution. + +Regression for sgl-project/sglang#23457: runtime attach must resolve +per-node local_hostname from process environment instead of a shared default. +""" + +import json +import os +import tempfile +import unittest + +from sglang.srt.mem_cache.storage.mooncake_store.mooncake_store import ( + MooncakeStoreConfig, +) +from sglang.srt.utils.common import temp_set_env +from sglang.test.ci.ci_register import register_cpu_ci +from sglang.test.test_utils import CustomTestCase + +register_cpu_ci(est_time=5, suite="base-a-test-cpu") + + +class TestMooncakeStoreConfigLocalHostname(CustomTestCase): + _BASE_EXTRA_CONFIG = {"master_server_address": "127.0.0.1:50051"} + + def _load_from_extra_config(self, extra_config=None): + config = {**self._BASE_EXTRA_CONFIG, **(extra_config or {})} + return MooncakeStoreConfig.load_from_extra_config(config) + + def test_load_from_extra_config_uses_mooncake_env_when_omitted(self): + with temp_set_env(MOONCAKE_LOCAL_HOSTNAME="10.0.0.2"): + cfg = self._load_from_extra_config() + self.assertEqual(cfg.local_hostname, "10.0.0.2") + + def test_load_from_extra_config_uses_local_hostname_env_when_omitted(self): + with temp_set_env(LOCAL_HOSTNAME="10.0.0.3"): + cfg = self._load_from_extra_config() + self.assertEqual(cfg.local_hostname, "10.0.0.3") + + def test_load_from_extra_config_prefers_mooncake_env_over_local_hostname(self): + with temp_set_env( + MOONCAKE_LOCAL_HOSTNAME="10.0.0.7", LOCAL_HOSTNAME="10.0.0.8" + ): + cfg = self._load_from_extra_config() + self.assertEqual(cfg.local_hostname, "10.0.0.7") + + def test_load_from_extra_config_defaults_to_localhost_without_env(self): + with temp_set_env(MOONCAKE_LOCAL_HOSTNAME=None, LOCAL_HOSTNAME=None): + cfg = self._load_from_extra_config() + self.assertEqual(cfg.local_hostname, "localhost") + + def test_load_from_extra_config_uses_explicit_override_without_env(self): + with temp_set_env(MOONCAKE_LOCAL_HOSTNAME=None, LOCAL_HOSTNAME=None): + cfg = self._load_from_extra_config({"local_hostname": "10.0.0.9"}) + self.assertEqual(cfg.local_hostname, "10.0.0.9") + + def test_load_from_extra_config_prefers_env_over_broadcast_override(self): + with temp_set_env(MOONCAKE_LOCAL_HOSTNAME="10.0.0.4"): + cfg = self._load_from_extra_config({"local_hostname": "10.0.0.1"}) + self.assertEqual(cfg.local_hostname, "10.0.0.4") + + def test_load_from_env_uses_mooncake_env(self): + with temp_set_env( + MOONCAKE_LOCAL_HOSTNAME="10.0.0.5", MOONCAKE_MASTER="127.0.0.1:50051" + ): + cfg = MooncakeStoreConfig.load_from_env() + self.assertEqual(cfg.local_hostname, "10.0.0.5") + + def test_load_from_env_uses_local_hostname_env(self): + with temp_set_env( + LOCAL_HOSTNAME="10.0.0.10", MOONCAKE_MASTER="127.0.0.1:50051" + ): + cfg = MooncakeStoreConfig.load_from_env() + self.assertEqual(cfg.local_hostname, "10.0.0.10") + + def test_from_file_uses_process_env_when_local_hostname_omitted(self): + with tempfile.NamedTemporaryFile("w", delete=False) as fin: + json.dump( + { + "master_server_address": "127.0.0.1:50051", + "metadata_server": "P2PHANDSHAKE", + }, + fin, + ) + config_path = fin.name + + try: + with temp_set_env( + allow_sglang=True, + SGLANG_HICACHE_MOONCAKE_CONFIG_PATH=config_path, + MOONCAKE_LOCAL_HOSTNAME="10.0.0.6", + ): + cfg = MooncakeStoreConfig.from_file() + self.assertEqual(cfg.local_hostname, "10.0.0.6") + finally: + os.unlink(config_path) + + def test_from_file_prefers_env_over_file_local_hostname(self): + with tempfile.NamedTemporaryFile("w", delete=False) as fin: + json.dump( + { + "master_server_address": "127.0.0.1:50051", + "metadata_server": "P2PHANDSHAKE", + "local_hostname": "10.0.0.1", + }, + fin, + ) + config_path = fin.name + + try: + with temp_set_env( + allow_sglang=True, + SGLANG_HICACHE_MOONCAKE_CONFIG_PATH=config_path, + MOONCAKE_LOCAL_HOSTNAME="10.0.0.11", + ): + cfg = MooncakeStoreConfig.from_file() + self.assertEqual(cfg.local_hostname, "10.0.0.11") + finally: + os.unlink(config_path) + + +if __name__ == "__main__": + unittest.main()