Fix NIXL cleaner grouping for hybrid cache keys (#35130)

Co-authored-by: Wei Yang <yawei@microsoft.com>
This commit is contained in:
Wei Yang
2026-08-18 13:55:51 -05:00
committed by GitHub
co-authored by Wei Yang
parent 526af15845
commit 8bb106cee9
2 changed files with 75 additions and 18 deletions
@@ -11,6 +11,7 @@ import time
from dataclasses import dataclass, field
from typing import Iterable, Optional
from sglang.srt.mem_cache.hicache_storage import PoolName
from sglang.srt.mem_cache.storage.nixl.nixl_routing import BUCKET_HEX_CHARS
logger = logging.getLogger(__name__)
@@ -21,6 +22,12 @@ _DEFAULT_HIGH_WATERMARK = 80.0
_DEFAULT_LOW_WATERMARK = 70.0
_RANK_SUFFIX_RE = re.compile(r"_(\d+)_(\d+)$")
_KV_SUFFIXES = ("_k", "_v")
_POOL_NAME_PATTERN = "|".join(
sorted((re.escape(pool.value) for pool in PoolName), key=len, reverse=True)
)
_HYBRID_COMPONENT_SUFFIX_RE = re.compile(
rf"_(?:{_POOL_NAME_PATTERN})(?:_(?:temporal|conv_\d+|[kv]|\d+))?$"
)
_BUCKET_NAME_RE = re.compile(rf"^[0-9a-f]{{{BUCKET_HEX_CHARS}}}$")
@@ -40,10 +47,12 @@ class _GroupInfo:
def _parse_group_key(name: str) -> str:
"""Return the logical cache-key group for one NIXL FILE object name.
The physical names are produced by ``HiCacheNixl._get_suffixed_key`` and
``HiCacheNixl._get_key_list_from_meta``.
The physical names are produced by ``HiCacheNixl._get_suffixed_key``,
``HiCacheNixl._get_key_list_from_meta``, and
``HiCacheNixl._get_hybrid_component_keys``.
"""
stem = name
stem = _HYBRID_COMPONENT_SUFFIX_RE.sub("", stem)
for suffix in _KV_SUFFIXES:
if stem.endswith(suffix):
stem = stem[: -len(suffix)]
@@ -40,20 +40,7 @@ class TestHiCacheL3Cleaner(CustomTestCase):
os.utime(path, (mtime, mtime))
return path
def test_parse_group_key_strips_rank_and_kv_suffix(self):
"""Keys for TP ranks and zero-copy K/V files share one cleanup group."""
self.assertEqual(_parse_group_key("page-a_model_0_8"), "page-a_model")
self.assertEqual(_parse_group_key("page-a_model_7_8_k"), "page-a_model")
self.assertEqual(_parse_group_key("page-a_model_7_8_v"), "page-a_model")
self.assertEqual(_parse_group_key("page-a_model_k"), "page-a_model")
def test_tick_deletes_oldest_group_across_bucketed_dirs(self):
"""A cleaner batch deletes all files in the oldest logical key group."""
old_keys = ["page-old_model_0_2", "page-old_model_1_2"]
new_keys = ["page-new_model_0_2", "page-new_model_1_2"]
old_paths = [self._write_key(key, mtime=100.0) for key in old_keys]
new_paths = [self._write_key(key, mtime=200.0) for key in new_keys]
def _run_single_group_cleanup(self) -> None:
cleaner = HiCacheL3Cleaner(
self.base_dirs,
tp_rank=0,
@@ -62,7 +49,6 @@ class TestHiCacheL3Cleaner(CustomTestCase):
recheck_groups=1,
unlink_workers=1,
)
usage_calls: dict[str, int] = {}
def fake_usage(path: str) -> float:
@@ -70,8 +56,70 @@ class TestHiCacheL3Cleaner(CustomTestCase):
return 90.0 if usage_calls[path] == 1 else 60.0
cleaner._disk_usage_pct = fake_usage
self.assertTrue(cleaner._tick())
def test_parse_group_key_strips_rank_and_kv_suffix(self):
"""Keys for TP ranks and zero-copy K/V files share one cleanup group."""
self.assertEqual(_parse_group_key("page-a_model_0_8"), "page-a_model")
self.assertEqual(_parse_group_key("page-a_model_7_8_k"), "page-a_model")
self.assertEqual(_parse_group_key("page-a_model_7_8_v"), "page-a_model")
self.assertEqual(_parse_group_key("page-a_model_k"), "page-a_model")
def test_parse_group_key_strips_hybrid_component_suffix(self):
"""All hybrid component shapes share the logical page's cleanup group."""
names = [
"page-a_model_7_8_kv_k",
"page-a_model_7_8_swa_k",
"page-a_model_7_8_swa_v",
"page-a_model_7_8_mamba_temporal",
"page-a_model_7_8_mamba_conv_0",
"page-a_model_7_8_indexer_2",
"page-a_model_7_8_draft_swa",
"page-a_model_deepseek_v4_c4_indexer_state_2",
]
for name in names:
with self.subTest(name=name):
self.assertEqual(_parse_group_key(name), "page-a_model")
def test_tick_deletes_oldest_group_across_bucketed_dirs(self):
"""A cleaner batch deletes all files in the oldest logical key group."""
old_keys = ["page-old_model_0_2", "page-old_model_1_2"]
new_keys = ["page-new_model_0_2", "page-new_model_1_2"]
old_paths = [self._write_key(key, mtime=100.0) for key in old_keys]
new_paths = [self._write_key(key, mtime=200.0) for key in new_keys]
self._run_single_group_cleanup()
self.assertFalse(any(os.path.exists(path) for path in old_paths))
self.assertTrue(all(os.path.exists(path) for path in new_paths))
def test_tick_deletes_hybrid_components_atomically(self):
"""Evict every pool component and TP rank for one logical page."""
physical_suffixes = [
"",
"_k",
"_v",
"_kv_k",
"_kv_v",
"_swa_k",
"_swa_v",
"_mamba_temporal",
"_mamba_conv_0",
]
old_keys = [
f"page-old_model_{rank}_2{suffix}"
for rank in range(2)
for suffix in physical_suffixes
]
new_keys = [
f"page-new_model_{rank}_2{suffix}"
for rank in range(2)
for suffix in physical_suffixes
]
old_paths = [self._write_key(key, mtime=100.0) for key in old_keys]
new_paths = [self._write_key(key, mtime=200.0) for key in new_keys]
self._run_single_group_cleanup()
self.assertFalse(any(os.path.exists(path) for path in old_paths))
self.assertTrue(all(os.path.exists(path) for path in new_paths))