[Mamba] Add a per-path cap for cached states (#31230)
This commit is contained in:
@@ -54,6 +54,7 @@ class MambaComponent(TreeComponent):
|
|||||||
super().__init__(cache, params)
|
super().__init__(cache, params)
|
||||||
self.enable_mamba_extra_buffer = params.enable_mamba_extra_buffer
|
self.enable_mamba_extra_buffer = params.enable_mamba_extra_buffer
|
||||||
self.enable_mamba_extra_buffer_lazy = params.enable_mamba_extra_buffer_lazy
|
self.enable_mamba_extra_buffer_lazy = params.enable_mamba_extra_buffer_lazy
|
||||||
|
self.mamba_max_states_per_path = get_server_args().mamba_max_states_per_path
|
||||||
# HiCache state
|
# HiCache state
|
||||||
self._mamba_pool_host = None # set to host mamba pool when HiCache enabled
|
self._mamba_pool_host = None # set to host mamba pool when HiCache enabled
|
||||||
|
|
||||||
@@ -161,6 +162,7 @@ class MambaComponent(TreeComponent):
|
|||||||
self.cache.component_evictable_size_[self.component_type] += len(
|
self.cache.component_evictable_size_[self.component_type] += len(
|
||||||
params.mamba_value
|
params.mamba_value
|
||||||
)
|
)
|
||||||
|
self._evict_excess_path_states(node)
|
||||||
return
|
return
|
||||||
if node.component_data[self.component_type].value is None:
|
if node.component_data[self.component_type].value is None:
|
||||||
node.component_data[self.component_type].value = params.mamba_value
|
node.component_data[self.component_type].value = params.mamba_value
|
||||||
@@ -173,11 +175,49 @@ class MambaComponent(TreeComponent):
|
|||||||
params.mamba_value
|
params.mamba_value
|
||||||
)
|
)
|
||||||
node.last_access_time = get_and_increase_time_counter()
|
node.last_access_time = get_and_increase_time_counter()
|
||||||
|
self._evict_excess_path_states(node)
|
||||||
return
|
return
|
||||||
self.cache.lru_lists[self.component_type].reset_node_mru(node)
|
self.cache.lru_lists[self.component_type].reset_node_mru(node)
|
||||||
node.last_access_time = get_and_increase_time_counter()
|
node.last_access_time = get_and_increase_time_counter()
|
||||||
result.mamba_exist = True
|
result.mamba_exist = True
|
||||||
|
|
||||||
|
def _evict_excess_path_states(self, tail: UnifiedTreeNode) -> None:
|
||||||
|
"""Evict shallow eligible device checkpoints beyond the path cap.
|
||||||
|
|
||||||
|
Full KV and any existing host backup are retained. The tail, forks,
|
||||||
|
locked nodes, and device leaves are preserved, so the cap is a
|
||||||
|
best-effort soft limit.
|
||||||
|
"""
|
||||||
|
cap = self.mamba_max_states_per_path
|
||||||
|
if cap < 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
ct = self.component_type
|
||||||
|
holders = []
|
||||||
|
node = tail
|
||||||
|
while node is not None and node is not self.cache.root_node:
|
||||||
|
if node.component_data[ct].value is not None:
|
||||||
|
holders.append(node)
|
||||||
|
node = node.parent
|
||||||
|
|
||||||
|
excess = len(holders) - cap
|
||||||
|
if excess <= 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
tracker = {component: 0 for component in self.cache.tree_components}
|
||||||
|
for node in reversed(holders):
|
||||||
|
if excess <= 0 or node is tail:
|
||||||
|
break
|
||||||
|
if node.component_data[ct].lock_ref > 0 or len(node.children) != 1:
|
||||||
|
continue
|
||||||
|
if node in self.cache.evictable_device_leaves:
|
||||||
|
continue
|
||||||
|
self.cache._evict_component_and_detach_lru(
|
||||||
|
node, self, target=EvictLayer.DEVICE, tracker=tracker
|
||||||
|
)
|
||||||
|
self.cache._cascade_evict(node, self, tracker)
|
||||||
|
excess -= 1
|
||||||
|
|
||||||
def redistribute_on_node_split(
|
def redistribute_on_node_split(
|
||||||
self, new_parent: UnifiedTreeNode, child: UnifiedTreeNode
|
self, new_parent: UnifiedTreeNode, child: UnifiedTreeNode
|
||||||
):
|
):
|
||||||
|
|||||||
@@ -2383,6 +2383,13 @@ class ServerArgs:
|
|||||||
),
|
),
|
||||||
NS("exec.mamba"),
|
NS("exec.mamba"),
|
||||||
] = None
|
] = None
|
||||||
|
mamba_max_states_per_path: A[
|
||||||
|
int,
|
||||||
|
"Maximum number of cached Mamba states retained per root-to-tail path "
|
||||||
|
"(-1 means unlimited). When enabled, after each insert the shallowest eligible "
|
||||||
|
"interior states beyond the cap are removed while their full KV remains. "
|
||||||
|
"Tail, fork, and locked nodes are preserved. Must be -1 or a positive integer.",
|
||||||
|
] = -1
|
||||||
enable_mamba_cache_stochastic_rounding: A[
|
enable_mamba_cache_stochastic_rounding: A[
|
||||||
bool,
|
bool,
|
||||||
"Enable stochastic rounding when writing FP16 Mamba SSM cache states. Requires --mamba-ssm-dtype float16 and CUDA. With --mamba-backend triton, requires SM100.",
|
"Enable stochastic rounding when writing FP16 Mamba SSM cache states. Requires --mamba-ssm-dtype float16 and CUDA. With --mamba-backend triton, requires SM100.",
|
||||||
@@ -3310,6 +3317,8 @@ class ServerArgs:
|
|||||||
# _handle_model_specific_adjustments never runs.
|
# _handle_model_specific_adjustments never runs.
|
||||||
self._resolved_overrides = []
|
self._resolved_overrides = []
|
||||||
|
|
||||||
|
self._validate_mamba_max_states_per_path()
|
||||||
|
|
||||||
if self.model_path.lower() in ["none", "dummy"]:
|
if self.model_path.lower() in ["none", "dummy"]:
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -3487,6 +3496,14 @@ class ServerArgs:
|
|||||||
|
|
||||||
materialize_declarations(self)
|
materialize_declarations(self)
|
||||||
|
|
||||||
|
def _validate_mamba_max_states_per_path(self):
|
||||||
|
value = self.mamba_max_states_per_path
|
||||||
|
if value == 0 or value < -1:
|
||||||
|
raise ValueError(
|
||||||
|
"--mamba-max-states-per-path must be -1 (unlimited) or a positive "
|
||||||
|
f"integer, got {value}."
|
||||||
|
)
|
||||||
|
|
||||||
def _handle_model_capability_adjustments(self):
|
def _handle_model_capability_adjustments(self):
|
||||||
if parse_connector_type(self.model_path) == ConnectorType.INSTANCE:
|
if parse_connector_type(self.model_path) == ConnectorType.INSTANCE:
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -60,6 +60,8 @@ class TestUnifiedMambaRadixCache(UnifiedRadixTreeTestMixin, CustomTestCase):
|
|||||||
"extra_buffer",
|
"extra_buffer",
|
||||||
"--mamba-track-interval",
|
"--mamba-track-interval",
|
||||||
str(MAMBA_TRACK_INTERVAL),
|
str(MAMBA_TRACK_INTERVAL),
|
||||||
|
"--mamba-max-states-per-path",
|
||||||
|
"3",
|
||||||
],
|
],
|
||||||
env={"SGLANG_ENABLE_UNIFIED_RADIX_TREE": "1"},
|
env={"SGLANG_ENABLE_UNIFIED_RADIX_TREE": "1"},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,179 @@
|
|||||||
|
"""CPU-only unit tests for the per-path Mamba checkpoint cap."""
|
||||||
|
|
||||||
|
from sglang.test.ci.ci_register import register_cpu_ci
|
||||||
|
|
||||||
|
register_cpu_ci(est_time=2, suite="base-a-test-cpu")
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import unittest
|
||||||
|
from types import SimpleNamespace
|
||||||
|
|
||||||
|
import torch
|
||||||
|
|
||||||
|
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.server_args import ServerArgs
|
||||||
|
|
||||||
|
|
||||||
|
class _RecordingAllocator:
|
||||||
|
def __init__(self):
|
||||||
|
self.freed = []
|
||||||
|
|
||||||
|
def free(self, value):
|
||||||
|
self.freed.extend(value.tolist())
|
||||||
|
|
||||||
|
|
||||||
|
class _FakeUnifiedCache:
|
||||||
|
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 = {
|
||||||
|
ComponentType.MAMBA: UnifiedLRUList(
|
||||||
|
ComponentType.MAMBA, self.tree_components
|
||||||
|
)
|
||||||
|
}
|
||||||
|
self.host_lru_lists = {
|
||||||
|
ComponentType.MAMBA: UnifiedLRUList(
|
||||||
|
ComponentType.MAMBA, self.tree_components
|
||||||
|
)
|
||||||
|
}
|
||||||
|
self.evicted = []
|
||||||
|
self.cascaded = []
|
||||||
|
|
||||||
|
def _evict_component_and_detach_lru(self, node, component, **kwargs):
|
||||||
|
self.evicted.append(node)
|
||||||
|
return UnifiedRadixCache._evict_component_and_detach_lru(
|
||||||
|
self, node, component, **kwargs
|
||||||
|
)
|
||||||
|
|
||||||
|
def _cascade_evict(self, node, component, tracker):
|
||||||
|
self.cascaded.append(node)
|
||||||
|
|
||||||
|
|
||||||
|
def _build_unified_chain(cap, length=3):
|
||||||
|
cache = _FakeUnifiedCache()
|
||||||
|
component = object.__new__(MambaComponent)
|
||||||
|
component.cache = cache
|
||||||
|
component.mamba_max_states_per_path = cap
|
||||||
|
|
||||||
|
nodes = []
|
||||||
|
parent = cache.root_node
|
||||||
|
for index in range(length):
|
||||||
|
node = UnifiedTreeNode(cache.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)
|
||||||
|
nodes.append(node)
|
||||||
|
parent = node
|
||||||
|
return component, nodes, cache
|
||||||
|
|
||||||
|
|
||||||
|
class TestMambaPathStateCap(unittest.TestCase):
|
||||||
|
def test_server_arg_defaults_to_unlimited(self):
|
||||||
|
self.assertEqual(
|
||||||
|
ServerArgs(model_path="dummy").mamba_max_states_per_path,
|
||||||
|
-1,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_server_arg_cli(self):
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
ServerArgs.add_cli_args(parser)
|
||||||
|
|
||||||
|
args = parser.parse_args(
|
||||||
|
["--model-path", "dummy", "--mamba-max-states-per-path", "3"]
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(args.mamba_max_states_per_path, 3)
|
||||||
|
|
||||||
|
def test_server_arg_rejects_zero_and_values_below_negative_one(self):
|
||||||
|
for value in (0, -2):
|
||||||
|
with self.subTest(value=value), self.assertRaisesRegex(
|
||||||
|
ValueError,
|
||||||
|
"must be -1 \\(unlimited\\) or a positive integer",
|
||||||
|
):
|
||||||
|
ServerArgs(
|
||||||
|
model_path="dummy",
|
||||||
|
mamba_max_states_per_path=value,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_unified_cache_removes_only_shallow_mamba_state(self):
|
||||||
|
component, nodes, cache = _build_unified_chain(cap=2)
|
||||||
|
|
||||||
|
component._evict_excess_path_states(nodes[-1])
|
||||||
|
|
||||||
|
self.assertEqual(cache.evicted, [nodes[0]])
|
||||||
|
self.assertEqual(cache.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,
|
||||||
|
[0],
|
||||||
|
)
|
||||||
|
self.assertEqual(cache.component_evictable_size_[ComponentType.MAMBA], 2)
|
||||||
|
self.assertFalse(cache.lru_lists[ComponentType.MAMBA].in_list(nodes[0]))
|
||||||
|
self.assertTrue(
|
||||||
|
all(
|
||||||
|
node.component_data[ComponentType.FULL].value is not None
|
||||||
|
for node in nodes
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
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)
|
||||||
|
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])
|
||||||
|
|
||||||
|
self.assertEqual(cache.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)
|
||||||
|
mamba_data = nodes[0].component_data[ComponentType.MAMBA]
|
||||||
|
mamba_data.host_value = torch.tensor([10])
|
||||||
|
|
||||||
|
component._evict_excess_path_states(nodes[-1])
|
||||||
|
|
||||||
|
self.assertIsNone(mamba_data.value)
|
||||||
|
self.assertIsNotNone(mamba_data.host_value)
|
||||||
|
self.assertTrue(cache.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._evict_excess_path_states(nodes[-1])
|
||||||
|
|
||||||
|
self.assertEqual(cache.evicted, [])
|
||||||
|
self.assertTrue(
|
||||||
|
all(
|
||||||
|
node.component_data[ComponentType.MAMBA].value is not None
|
||||||
|
for node in nodes
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user