Radix Cache Split: Spin off TreeCore (#29901)
This commit is contained in:
@@ -1,44 +1,37 @@
|
||||
"""CPU-only unit tests for the per-path Mamba checkpoint cap."""
|
||||
|
||||
from sglang.test.ci.ci_register import register_cpu_ci
|
||||
from sglang.test.ci.ci_register import register_cpu_ci, register_cuda_ci
|
||||
|
||||
register_cpu_ci(est_time=2, suite="base-a-test-cpu")
|
||||
register_cuda_ci(est_time=5, stage="base-b", runner_config="1-gpu-small")
|
||||
|
||||
import argparse
|
||||
import unittest
|
||||
from types import SimpleNamespace
|
||||
from collections import defaultdict
|
||||
from unittest import mock
|
||||
|
||||
import torch
|
||||
from test_unified_radix_cache_unittest import CacheConfig, UnifiedRadixCacheSuite
|
||||
|
||||
from sglang.srt.mem_cache.unified_cache.cache_action import MambaEvictExcessPathStates
|
||||
from sglang.srt.mem_cache.unified_cache.unified_tree_core import UnifiedTreeCore
|
||||
from sglang.srt.mem_cache.unified_cache_components.mamba_component import (
|
||||
MambaComponent,
|
||||
)
|
||||
from sglang.srt.mem_cache.unified_cache_components.tree_component import (
|
||||
ComponentType,
|
||||
)
|
||||
from sglang.srt.mem_cache.unified_radix_cache import (
|
||||
UnifiedLRUList,
|
||||
UnifiedRadixCache,
|
||||
UnifiedTreeNode,
|
||||
)
|
||||
from sglang.srt.mem_cache.unified_radix_cache import UnifiedLRUList, UnifiedTreeNode
|
||||
from sglang.srt.server_args import ServerArgs
|
||||
from sglang.test.test_utils import CustomTestCase
|
||||
|
||||
|
||||
class _RecordingAllocator:
|
||||
def __init__(self):
|
||||
self.freed = []
|
||||
|
||||
def free(self, value):
|
||||
self.freed.extend(value.tolist())
|
||||
|
||||
|
||||
class _FakeUnifiedCache:
|
||||
class _FakeTreeCore:
|
||||
tree_components = (ComponentType.FULL, ComponentType.MAMBA)
|
||||
|
||||
def __init__(self):
|
||||
self.root_node = UnifiedTreeNode(self.tree_components)
|
||||
self.evictable_device_leaves = set()
|
||||
self.req_to_token_pool = SimpleNamespace(mamba_allocator=_RecordingAllocator())
|
||||
self.component_evictable_size_ = {ComponentType.MAMBA: 0}
|
||||
self.component_protected_size_ = {ComponentType.MAMBA: 0}
|
||||
self.lru_lists = {
|
||||
@@ -48,41 +41,47 @@ class _FakeUnifiedCache:
|
||||
}
|
||||
self.host_lru_lists = {
|
||||
ComponentType.MAMBA: UnifiedLRUList(
|
||||
ComponentType.MAMBA, self.tree_components
|
||||
ComponentType.MAMBA, self.tree_components, use_host_ptr=True
|
||||
)
|
||||
}
|
||||
self.evicted = []
|
||||
self.cascaded = []
|
||||
|
||||
def _evict_component_and_detach_lru(self, node, component, **kwargs):
|
||||
def _evict_component_and_detach_lru(self, node, component, *args, **kwargs):
|
||||
self.evicted.append(node)
|
||||
return UnifiedRadixCache._evict_component_and_detach_lru(
|
||||
self, node, component, **kwargs
|
||||
return UnifiedTreeCore._evict_component_and_detach_lru(
|
||||
self, node, component, *args, **kwargs
|
||||
)
|
||||
|
||||
def _cascade_evict(self, node, component, tracker):
|
||||
def _cascade_evict(self, node, component, tracker, device_frees, host_frees):
|
||||
self.cascaded.append(node)
|
||||
|
||||
|
||||
class _FakeUnifiedCache:
|
||||
tree_components = _FakeTreeCore.tree_components
|
||||
|
||||
|
||||
def _build_unified_chain(cap, length=3):
|
||||
cache = _FakeUnifiedCache()
|
||||
core = _FakeTreeCore()
|
||||
component = object.__new__(MambaComponent)
|
||||
component.cache = cache
|
||||
component.tree_core = core
|
||||
component.mamba_max_states_per_path = cap
|
||||
|
||||
nodes = []
|
||||
parent = cache.root_node
|
||||
parent = core.root_node
|
||||
for index in range(length):
|
||||
node = UnifiedTreeNode(cache.tree_components)
|
||||
node = UnifiedTreeNode(core.tree_components)
|
||||
node.parent = parent
|
||||
node.component_data[ComponentType.FULL].value = torch.tensor([100 + index])
|
||||
node.component_data[ComponentType.MAMBA].value = torch.tensor([index])
|
||||
parent.children[index] = node
|
||||
cache.component_evictable_size_[ComponentType.MAMBA] += 1
|
||||
cache.lru_lists[ComponentType.MAMBA].insert_mru(node)
|
||||
core.component_evictable_size_[ComponentType.MAMBA] += 1
|
||||
core.lru_lists[ComponentType.MAMBA].insert_mru(node)
|
||||
nodes.append(node)
|
||||
parent = node
|
||||
return component, nodes, cache
|
||||
return component, nodes, core, cache
|
||||
|
||||
|
||||
class TestMambaPathStateCap(unittest.TestCase):
|
||||
@@ -114,20 +113,22 @@ class TestMambaPathStateCap(unittest.TestCase):
|
||||
)
|
||||
|
||||
def test_unified_cache_removes_only_shallow_mamba_state(self):
|
||||
component, nodes, cache = _build_unified_chain(cap=2)
|
||||
component, nodes, core, cache = _build_unified_chain(cap=2)
|
||||
|
||||
component._evict_excess_path_states(nodes[-1])
|
||||
device_frees = defaultdict(list)
|
||||
host_frees = defaultdict(list)
|
||||
component._evict_excess_path_states(nodes[-1], device_frees, host_frees)
|
||||
|
||||
self.assertEqual(cache.evicted, [nodes[0]])
|
||||
self.assertEqual(cache.cascaded, [nodes[0]])
|
||||
self.assertEqual(core.evicted, [nodes[0]])
|
||||
self.assertEqual(core.cascaded, [nodes[0]])
|
||||
self.assertIsNone(nodes[0].component_data[ComponentType.MAMBA].value)
|
||||
self.assertIsNotNone(nodes[-1].component_data[ComponentType.MAMBA].value)
|
||||
self.assertEqual(
|
||||
cache.req_to_token_pool.mamba_allocator.freed,
|
||||
[v.item() for v in device_frees[ComponentType.MAMBA]],
|
||||
[0],
|
||||
)
|
||||
self.assertEqual(cache.component_evictable_size_[ComponentType.MAMBA], 2)
|
||||
self.assertFalse(cache.lru_lists[ComponentType.MAMBA].in_list(nodes[0]))
|
||||
self.assertEqual(core.component_evictable_size_[ComponentType.MAMBA], 2)
|
||||
self.assertFalse(core.lru_lists[ComponentType.MAMBA].in_list(nodes[0]))
|
||||
self.assertTrue(
|
||||
all(
|
||||
node.component_data[ComponentType.FULL].value is not None
|
||||
@@ -136,37 +137,44 @@ class TestMambaPathStateCap(unittest.TestCase):
|
||||
)
|
||||
|
||||
def test_unified_cache_cap_is_soft_for_fork_and_locked_nodes(self):
|
||||
component, nodes, cache = _build_unified_chain(cap=1, length=4)
|
||||
fork_child = UnifiedTreeNode(cache.tree_components)
|
||||
component, nodes, core, cache = _build_unified_chain(cap=1, length=4)
|
||||
fork_child = UnifiedTreeNode(core.tree_components)
|
||||
fork_child.parent = nodes[0]
|
||||
nodes[0].children["fork"] = fork_child
|
||||
nodes[1].component_data[ComponentType.MAMBA].lock_ref = 1
|
||||
|
||||
component._evict_excess_path_states(nodes[-1])
|
||||
device_frees = defaultdict(list)
|
||||
host_frees = defaultdict(list)
|
||||
component._evict_excess_path_states(nodes[-1], device_frees, host_frees)
|
||||
|
||||
self.assertEqual(cache.evicted, [nodes[2]])
|
||||
self.assertEqual(core.evicted, [nodes[2]])
|
||||
self.assertIsNotNone(nodes[0].component_data[ComponentType.MAMBA].value)
|
||||
self.assertIsNotNone(nodes[1].component_data[ComponentType.MAMBA].value)
|
||||
self.assertIsNone(nodes[2].component_data[ComponentType.MAMBA].value)
|
||||
self.assertIsNotNone(nodes[3].component_data[ComponentType.MAMBA].value)
|
||||
|
||||
def test_unified_cache_preserves_existing_host_backup(self):
|
||||
component, nodes, cache = _build_unified_chain(cap=2)
|
||||
component, nodes, core, cache = _build_unified_chain(cap=2)
|
||||
mamba_data = nodes[0].component_data[ComponentType.MAMBA]
|
||||
mamba_data.host_value = torch.tensor([10])
|
||||
|
||||
component._evict_excess_path_states(nodes[-1])
|
||||
device_frees = defaultdict(list)
|
||||
host_frees = defaultdict(list)
|
||||
component._evict_excess_path_states(nodes[-1], device_frees, host_frees)
|
||||
|
||||
self.assertIsNone(mamba_data.value)
|
||||
self.assertIsNotNone(mamba_data.host_value)
|
||||
self.assertTrue(cache.host_lru_lists[ComponentType.MAMBA].in_list(nodes[0]))
|
||||
self.assertTrue(core.host_lru_lists[ComponentType.MAMBA].in_list(nodes[0]))
|
||||
|
||||
def test_unified_cache_negative_one_disables_cap(self):
|
||||
component, nodes, cache = _build_unified_chain(cap=-1)
|
||||
component, nodes, core, cache = _build_unified_chain(cap=-1)
|
||||
|
||||
component._evict_excess_path_states(nodes[-1])
|
||||
device_frees = defaultdict(list)
|
||||
host_frees = defaultdict(list)
|
||||
component._evict_excess_path_states(nodes[-1], device_frees, host_frees)
|
||||
|
||||
self.assertEqual(cache.evicted, [])
|
||||
self.assertEqual(dict(device_frees), {})
|
||||
self.assertEqual(core.evicted, [])
|
||||
self.assertTrue(
|
||||
all(
|
||||
node.component_data[ComponentType.MAMBA].value is not None
|
||||
@@ -175,5 +183,162 @@ class TestMambaPathStateCap(unittest.TestCase):
|
||||
)
|
||||
|
||||
|
||||
@unittest.skipUnless(torch.cuda.is_available(), "mamba pool fixtures need CUDA")
|
||||
class TestMambaPathCapWriteThroughOrdering(CustomTestCase):
|
||||
"""CI-active write-through/path-cap ordering regressions (the unified radix
|
||||
cache unittest module is temporarily gated off on trunk)."""
|
||||
|
||||
cfg = CacheConfig(components=(ComponentType.FULL, ComponentType.MAMBA))
|
||||
_rid = 0
|
||||
# Borrow the fixture helpers without inheriting the full gated suite.
|
||||
_make_req = UnifiedRadixCacheSuite._make_req
|
||||
_alloc = UnifiedRadixCacheSuite._alloc
|
||||
_insert = UnifiedRadixCacheSuite._insert
|
||||
_init_hicache = UnifiedRadixCacheSuite._init_hicache
|
||||
_build_hicache_fixture = UnifiedRadixCacheSuite._build_hicache_fixture
|
||||
|
||||
def test_write_through_backup_survives_mamba_path_cap(self):
|
||||
cache, allocator, req_to_token_pool = self._build_hicache_fixture()
|
||||
cache.write_through_threshold = 2
|
||||
cache.components[ComponentType.MAMBA].mamba_max_states_per_path = 1
|
||||
|
||||
# The first insert creates the ancestor with a mamba state (hit_count 1).
|
||||
self._insert(cache, allocator, req_to_token_pool, [1, 2])
|
||||
ancestor = next(iter(cache.root_node.children.values()))
|
||||
self.assertIsNotNone(ancestor.component_data[ComponentType.MAMBA].value)
|
||||
|
||||
# The extending insert crosses the ancestor's write-through threshold in
|
||||
# the same walk whose commit runs the path-cap eviction; the cap must
|
||||
# leave the pending-backup node's device state for the deferred BackupKV.
|
||||
self._insert(cache, allocator, req_to_token_pool, [1, 2, 3, 4])
|
||||
cache.writing_check(write_back=True)
|
||||
|
||||
self.assertTrue(ancestor.backuped)
|
||||
self.assertIsNotNone(ancestor.component_data[ComponentType.MAMBA].host_value)
|
||||
|
||||
def test_write_through_backup_chain_survives_mamba_path_cap(self):
|
||||
"""A failed backup leaves an unbacked ancestor inside a later deferred
|
||||
backup chain; the cap walk must spare the whole chain, not just its tip."""
|
||||
cache, allocator, req_to_token_pool = self._build_hicache_fixture()
|
||||
cache.write_through_threshold = 3
|
||||
mamba_comp = cache.components[ComponentType.MAMBA]
|
||||
|
||||
self._insert(cache, allocator, req_to_token_pool, [1, 2])
|
||||
ancestor = next(iter(cache.root_node.children.values()))
|
||||
self._insert(cache, allocator, req_to_token_pool, [1, 2, 3, 4])
|
||||
middle = next(iter(ancestor.children.values()))
|
||||
|
||||
# The ancestor crosses the threshold here; a host-exhaustion failure
|
||||
# leaves it unbacked with hit_count past the bar and its state intact.
|
||||
with mock.patch.object(cache, "_execute_kv_backup", return_value=None):
|
||||
self._insert(cache, allocator, req_to_token_pool, [1, 2, 3, 4, 5, 6])
|
||||
self.assertFalse(ancestor.backuped)
|
||||
self.assertIsNotNone(ancestor.component_data[ComponentType.MAMBA].value)
|
||||
|
||||
# The middle node crosses next, so the deferred chain is
|
||||
# [ancestor, middle]; the extending insert adopts a new leaf state,
|
||||
# firing the now-enabled cap walk before the chain executes — it must
|
||||
# not evict either chain node's device state.
|
||||
mamba_comp.mamba_max_states_per_path = 1
|
||||
self._insert(cache, allocator, req_to_token_pool, [1, 2, 3, 4, 5, 6, 7, 8])
|
||||
cache.writing_check(write_back=True)
|
||||
|
||||
self.assertTrue(ancestor.backuped)
|
||||
self.assertIsNotNone(ancestor.component_data[ComponentType.MAMBA].host_value)
|
||||
self.assertTrue(middle.backuped)
|
||||
self.assertIsNotNone(middle.component_data[ComponentType.MAMBA].host_value)
|
||||
|
||||
def test_backup_retry_after_mamba_cap_skips_tombstoned_state(self):
|
||||
"""A backup that fails before the cap and retries via the leaf action
|
||||
rebuilds its spec post-cap: KV backs up, the tombstoned mamba arm stays gone."""
|
||||
cache, allocator, req_to_token_pool = self._build_hicache_fixture()
|
||||
cache.write_through_threshold = 1
|
||||
mamba_comp = cache.components[ComponentType.MAMBA]
|
||||
|
||||
# A failed write-through leaves the ancestor unbacked with device state.
|
||||
with mock.patch.object(cache, "_execute_kv_backup", return_value=None):
|
||||
self._insert(cache, allocator, req_to_token_pool, [1, 2])
|
||||
ancestor = next(iter(cache.root_node.children.values()))
|
||||
self.assertFalse(ancestor.backuped)
|
||||
self.assertIsNotNone(ancestor.component_data[ComponentType.MAMBA].value)
|
||||
|
||||
# The walk backup fails again, the cap tombstones the unlocked
|
||||
# ancestor's mamba state, then the leaf-action retry succeeds.
|
||||
mamba_comp.mamba_max_states_per_path = 1
|
||||
real_backup = cache._execute_kv_backup
|
||||
attempts = []
|
||||
|
||||
def fail_once(*args, **kwargs):
|
||||
attempts.append(args)
|
||||
if len(attempts) == 1:
|
||||
return None
|
||||
return real_backup(*args, **kwargs)
|
||||
|
||||
with mock.patch.object(cache, "_execute_kv_backup", side_effect=fail_once):
|
||||
self._insert(cache, allocator, req_to_token_pool, [1, 2, 3, 4])
|
||||
leaf = next(iter(ancestor.children.values()))
|
||||
cache.writing_check(write_back=True)
|
||||
|
||||
# Post-cap spec rebuild: no resurrection of the tombstoned mamba state.
|
||||
self.assertTrue(ancestor.backuped)
|
||||
ancestor_cd = ancestor.component_data[ComponentType.MAMBA]
|
||||
self.assertIsNone(ancestor_cd.value)
|
||||
self.assertIsNone(ancestor_cd.host_value)
|
||||
self.assertTrue(leaf.backuped)
|
||||
self.assertIsNotNone(leaf.component_data[ComponentType.MAMBA].host_value)
|
||||
cache.sanity_check()
|
||||
|
||||
def test_walk_backup_excludes_same_insert_restamped_mamba(self):
|
||||
"""The walked target's backup executes before commit hooks, so a mamba
|
||||
value re-stamped by the same insert stays out of the host backup."""
|
||||
cache, allocator, req_to_token_pool = self._build_hicache_fixture()
|
||||
mamba_comp = cache.components[ComponentType.MAMBA]
|
||||
|
||||
self._insert(cache, allocator, req_to_token_pool, [1, 2])
|
||||
ancestor = next(iter(cache.root_node.children.values()))
|
||||
self._insert(cache, allocator, req_to_token_pool, [1, 2, 3, 4])
|
||||
|
||||
# The cap walk tombstones the ancestor's mamba state.
|
||||
mamba_comp.mamba_max_states_per_path = 1
|
||||
self._insert(cache, allocator, req_to_token_pool, [1, 2, 3, 4, 5, 6])
|
||||
self.assertIsNone(ancestor.component_data[ComponentType.MAMBA].value)
|
||||
|
||||
# Re-inserting [1, 2] crosses the threshold and re-stamps the tombstone
|
||||
# in the same insert; the backup must not carry the fresh mamba state.
|
||||
cache.write_through_threshold = ancestor.hit_count + 1
|
||||
self._insert(cache, allocator, req_to_token_pool, [1, 2])
|
||||
cache.writing_check(write_back=True)
|
||||
|
||||
self.assertTrue(ancestor.backuped)
|
||||
ancestor_cd = ancestor.component_data[ComponentType.MAMBA]
|
||||
self.assertIsNotNone(ancestor_cd.value)
|
||||
self.assertIsNone(ancestor_cd.host_value)
|
||||
|
||||
def test_cap_walk_failure_still_drains_collected_frees(self):
|
||||
"""A cap walk that raises mid-eviction must still free the tombstoned
|
||||
slots it already collected (the pre-split inline frees could not leak)."""
|
||||
cache, allocator, req_to_token_pool = self._build_hicache_fixture()
|
||||
mamba_comp = cache.components[ComponentType.MAMBA]
|
||||
|
||||
self._insert(cache, allocator, req_to_token_pool, [1, 2])
|
||||
ancestor = next(iter(cache.root_node.children.values()))
|
||||
self._insert(cache, allocator, req_to_token_pool, [1, 2, 3, 4])
|
||||
leaf = next(iter(ancestor.children.values()))
|
||||
self.assertIsNotNone(ancestor.component_data[ComponentType.MAMBA].value)
|
||||
|
||||
mamba_comp.mamba_max_states_per_path = 1
|
||||
available = req_to_token_pool.mamba_allocator.available_size()
|
||||
with mock.patch.object(
|
||||
cache.tree_core, "_cascade_evict", side_effect=RuntimeError("boom")
|
||||
):
|
||||
with self.assertRaises(RuntimeError):
|
||||
mamba_comp.apply_component_action(MambaEvictExcessPathStates(leaf.id))
|
||||
|
||||
self.assertIsNone(ancestor.component_data[ComponentType.MAMBA].value)
|
||||
self.assertEqual(
|
||||
req_to_token_pool.mamba_allocator.available_size(), available + 1
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
"""Unit tests for the tree-core backend registry."""
|
||||
|
||||
import unittest
|
||||
from unittest import mock
|
||||
|
||||
from sglang.srt.environ import envs
|
||||
from sglang.srt.mem_cache.cache_init_params import CacheInitParams
|
||||
from sglang.srt.mem_cache.memory_pool import ReqToTokenPool
|
||||
from sglang.srt.mem_cache.unified_cache.component_type import ComponentType
|
||||
from sglang.srt.mem_cache.unified_cache.tree_core_registry import (
|
||||
_TREE_CORE_REGISTRY,
|
||||
create_tree_core,
|
||||
register_tree_core_backend,
|
||||
registered_tree_core_backends,
|
||||
)
|
||||
from sglang.srt.mem_cache.unified_cache.unified_tree_core import UnifiedTreeCore
|
||||
from sglang.srt.mem_cache.unified_cache_components.tree_component import (
|
||||
EvictLayer,
|
||||
TreeComponent,
|
||||
)
|
||||
from sglang.srt.mem_cache.unified_radix_cache import UnifiedRadixCache
|
||||
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")
|
||||
|
||||
|
||||
def _cache_init_params(**kwargs) -> CacheInitParams:
|
||||
return CacheInitParams(
|
||||
disable=False,
|
||||
req_to_token_pool=None,
|
||||
token_to_kv_pool_allocator=None,
|
||||
page_size=2,
|
||||
tree_components=(ComponentType.FULL,),
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
class _StubFullComponent(TreeComponent):
|
||||
component_type = ComponentType.FULL
|
||||
|
||||
def create_match_validator(self, match_device_only: bool = False):
|
||||
return lambda node: True
|
||||
|
||||
def redistribute_on_node_split(self, new_parent, child):
|
||||
return None
|
||||
|
||||
def evict_component(
|
||||
self, node, device_frees, host_frees, target: EvictLayer = EvictLayer.DEVICE
|
||||
) -> tuple[int, int]:
|
||||
return 0, 0
|
||||
|
||||
def acquire_component_lock(self, node, result):
|
||||
return result
|
||||
|
||||
def release_component_lock(self, node, params):
|
||||
return None
|
||||
|
||||
def _evict_device_start(self, request_cnt) -> None:
|
||||
pass
|
||||
|
||||
def _evict_device_next_node(self, tracker, device_frees, host_frees):
|
||||
return None
|
||||
|
||||
def _evict_device_end(self) -> None:
|
||||
pass
|
||||
|
||||
|
||||
class _StubMambaComponent(_StubFullComponent):
|
||||
component_type = ComponentType.MAMBA
|
||||
|
||||
|
||||
class TreeCoreRegistryTest(CustomTestCase):
|
||||
def setUp(self):
|
||||
self._registry_snapshot = dict(_TREE_CORE_REGISTRY)
|
||||
|
||||
def tearDown(self):
|
||||
_TREE_CORE_REGISTRY.clear()
|
||||
_TREE_CORE_REGISTRY.update(self._registry_snapshot)
|
||||
|
||||
def test_registry_contains_python(self):
|
||||
self.assertIn("python", registered_tree_core_backends())
|
||||
|
||||
def test_python_backend_builds_the_python_tree(self):
|
||||
component = mock.MagicMock()
|
||||
core = create_tree_core(
|
||||
name="python",
|
||||
params=_cache_init_params(),
|
||||
components={ComponentType.FULL: component},
|
||||
)
|
||||
self.assertIsInstance(core, UnifiedTreeCore)
|
||||
self.assertIs(component.tree_core, core)
|
||||
|
||||
def test_unknown_backend_raises_naming_the_known_backends(self):
|
||||
with self.assertRaisesRegex(ValueError, "not registered") as cm:
|
||||
create_tree_core(
|
||||
name="not_a_real_backend",
|
||||
params=_cache_init_params(),
|
||||
components={},
|
||||
)
|
||||
self.assertIn("'python'", str(cm.exception))
|
||||
|
||||
def test_register_rejects_empty_name(self):
|
||||
with self.assertRaises(ValueError):
|
||||
register_tree_core_backend(" ", mock.MagicMock())
|
||||
|
||||
def test_register_rejects_duplicate_name(self):
|
||||
with self.assertRaises(ValueError):
|
||||
register_tree_core_backend("python", mock.MagicMock())
|
||||
|
||||
def test_create_dispatches_to_a_registered_factory(self):
|
||||
core = mock.MagicMock()
|
||||
factory = mock.MagicMock(return_value=core)
|
||||
register_tree_core_backend("custom", factory)
|
||||
params = _cache_init_params()
|
||||
components = {ComponentType.FULL: mock.MagicMock()}
|
||||
result = create_tree_core(name="custom", params=params, components=components)
|
||||
factory.assert_called_once_with(params, components)
|
||||
self.assertIs(result, core)
|
||||
|
||||
|
||||
class UnifiedRadixCacheTreeCoreSelectionTest(CustomTestCase):
|
||||
def setUp(self):
|
||||
self._registry_snapshot = dict(_TREE_CORE_REGISTRY)
|
||||
|
||||
def tearDown(self):
|
||||
_TREE_CORE_REGISTRY.clear()
|
||||
_TREE_CORE_REGISTRY.update(self._registry_snapshot)
|
||||
|
||||
def _cache_params(
|
||||
self,
|
||||
tree_components=(ComponentType.FULL,),
|
||||
component_registry_override={ComponentType.FULL: _StubFullComponent},
|
||||
**kwargs,
|
||||
) -> CacheInitParams:
|
||||
return CacheInitParams(
|
||||
disable=True,
|
||||
req_to_token_pool=ReqToTokenPool(
|
||||
size=2,
|
||||
max_context_len=8,
|
||||
device="cpu",
|
||||
enable_memory_saver=False,
|
||||
),
|
||||
token_to_kv_pool_allocator=None,
|
||||
page_size=1,
|
||||
tree_components=tree_components,
|
||||
component_registry_override=component_registry_override,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
def test_init_downgrades_is_eagle_when_mamba_is_enabled(self):
|
||||
params = self._cache_params(
|
||||
is_eagle=True,
|
||||
tree_components=(ComponentType.FULL, ComponentType.MAMBA),
|
||||
component_registry_override={
|
||||
ComponentType.FULL: _StubFullComponent,
|
||||
ComponentType.MAMBA: _StubMambaComponent,
|
||||
},
|
||||
)
|
||||
cache = UnifiedRadixCache(params)
|
||||
self.assertFalse(cache.tree_core.is_eagle)
|
||||
|
||||
def test_init_keeps_is_eagle_without_mamba(self):
|
||||
params = self._cache_params(is_eagle=True)
|
||||
cache = UnifiedRadixCache(params)
|
||||
self.assertTrue(cache.tree_core.is_eagle)
|
||||
|
||||
def test_default_backend_builds_the_python_tree_core(self):
|
||||
cache = UnifiedRadixCache(params=self._cache_params())
|
||||
self.assertIsInstance(cache.tree_core, UnifiedTreeCore)
|
||||
component = cache.components[ComponentType.FULL]
|
||||
self.assertIs(component.tree_core, cache.tree_core)
|
||||
|
||||
def test_env_var_routes_construction_to_the_selected_backend(self):
|
||||
"""SGLANG_UNIFIED_RADIX_TREE_CORE_BACKEND selects the registered factory
|
||||
the cache constructs its tree through."""
|
||||
core = mock.MagicMock()
|
||||
factory = mock.MagicMock(return_value=core)
|
||||
register_tree_core_backend("custom_env_backend", factory)
|
||||
with envs.SGLANG_UNIFIED_RADIX_TREE_CORE_BACKEND.override("custom_env_backend"):
|
||||
cache = UnifiedRadixCache(params=self._cache_params())
|
||||
factory.assert_called_once()
|
||||
self.assertIs(cache.tree_core, core)
|
||||
component = cache.components[ComponentType.FULL]
|
||||
self.assertIs(component.tree_core, core)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -566,7 +566,7 @@ def bench_lock_unlock(
|
||||
nodes = []
|
||||
for seq in env.seqs[: num_seqs // 2]:
|
||||
r = env.tree.match_prefix(MatchPrefixParams(key=RadixKey(array("q", seq))))
|
||||
if r.last_device_node != env.tree.root_node:
|
||||
if r.last_device_node != env.tree.root_node_handle():
|
||||
nodes.append(r.last_device_node)
|
||||
if not nodes:
|
||||
return BenchResult("lock_unlock", 0, 0, 0, [])
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user