Reset only the used mamba state on radix cache hit (#31643)
This commit is contained in:
@@ -19,7 +19,6 @@ limitations under the License.
|
||||
The radix tree data structure for managing the hybrid (full and Mamba) KV cache.
|
||||
"""
|
||||
|
||||
import heapq
|
||||
from array import array
|
||||
from collections import defaultdict
|
||||
from typing import TYPE_CHECKING, List, Optional, Tuple
|
||||
@@ -83,7 +82,11 @@ class TreeNode:
|
||||
self.full_lock_ref = 0
|
||||
self.mamba_lock_ref = 0
|
||||
# last access time is only used for sanity check. LRU is maintained by the lru list.
|
||||
# `last_access_time` tracks the full LRU (whole matched path is reused as prefix);
|
||||
# `mamba_last_access_time` tracks the mamba LRU, which only touches the single state
|
||||
# actually consumed per access, so the two orders diverge and need separate stamps.
|
||||
self.last_access_time = get_last_access_time()
|
||||
self.mamba_last_access_time = self.last_access_time
|
||||
|
||||
self.hit_count = 0
|
||||
self.host_ref_counter = 0
|
||||
@@ -172,10 +175,12 @@ class LRUList:
|
||||
self.prv = "mamba_prev"
|
||||
self.nxt = "mamba_next"
|
||||
self.lock_ref = "mamba_lock_ref"
|
||||
self.time_attr = "mamba_last_access_time"
|
||||
else:
|
||||
self.prv = "prev"
|
||||
self.nxt = "next"
|
||||
self.lock_ref = "full_lock_ref"
|
||||
self.time_attr = "last_access_time"
|
||||
# Initialize dummy head and tail nodes
|
||||
self.head = TreeNode() # Most recently used side
|
||||
self.tail = TreeNode() # Least recently used side
|
||||
@@ -226,6 +231,8 @@ class LRUList:
|
||||
assert (
|
||||
not self.mamba or node.mamba_value is not None
|
||||
), f"Resetting mamba tombstone node in mamba lru list: {node.id=}"
|
||||
if self.mamba:
|
||||
node.mamba_last_access_time = get_last_access_time()
|
||||
self._remove_node(node)
|
||||
self._add_node(node)
|
||||
|
||||
@@ -255,6 +262,8 @@ class LRUList:
|
||||
assert (
|
||||
node.id not in self.cache
|
||||
), f"Inserting node {node.id=} already in lru list, existing node: {self.cache[node.id].id=}"
|
||||
if self.mamba:
|
||||
node.mamba_last_access_time = get_last_access_time()
|
||||
self.cache[node.id] = node
|
||||
self._add_node(node)
|
||||
|
||||
@@ -330,21 +339,20 @@ class LRUList:
|
||||
msg = f"{self.mamba=} LRU list: "
|
||||
x_lru = self._get_lru()
|
||||
while x_lru is not None and x_lru.id in self.cache:
|
||||
msg += f"[{x_lru.id}] {x_lru.last_access_time:f} -> "
|
||||
msg += f"[{x_lru.id}] {getattr(x_lru, self.time_attr):f} -> "
|
||||
x_lru = getattr(x_lru, self.prv)
|
||||
print(msg)
|
||||
|
||||
if not tree_cache:
|
||||
return
|
||||
msg = f"{self.mamba=} Nodes (sorted by last_access_time): "
|
||||
msg = f"{self.mamba=} Nodes (sorted by {self.time_attr}): "
|
||||
if self.mamba:
|
||||
nodes = tree_cache._collect_nontombstone_nodes()
|
||||
else:
|
||||
nodes = tree_cache._collect_all_nodes()
|
||||
heapq.heapify(nodes)
|
||||
while len(nodes):
|
||||
x = heapq.heappop(nodes)
|
||||
msg += f"[{x.id}] {x.last_access_time:f} -> "
|
||||
nodes.sort(key=lambda n: getattr(n, self.time_attr))
|
||||
for x in nodes:
|
||||
msg += f"[{x.id}] {getattr(x, self.time_attr):f} -> "
|
||||
print(msg)
|
||||
|
||||
# Note: this is expensive, only use for debug
|
||||
@@ -364,8 +372,8 @@ class LRUList:
|
||||
# Note: this is expensive, only use for debug or idle check
|
||||
def sanity_check(self, tree_cache: MambaRadixCache):
|
||||
"""
|
||||
Check if the lru list is valid by rebuilding the lru list from the tree, heapifying it, and
|
||||
checking if the lru list is valid.
|
||||
Check the lru list is valid by rebuilding it from the tree, sorting by this list's
|
||||
access-time stamp, and checking the order matches the linked list.
|
||||
"""
|
||||
try:
|
||||
if self.mamba:
|
||||
@@ -374,16 +382,16 @@ class LRUList:
|
||||
nodes = tree_cache._collect_all_nodes()
|
||||
total_nodes = len(nodes)
|
||||
total_lru = len(self.cache)
|
||||
# heapify based on last_access_time
|
||||
heapq.heapify(nodes)
|
||||
# rebuild expected order from this list's own access-time stamp (full and mamba
|
||||
# lists have independent recency, so they use different stamps)
|
||||
nodes.sort(key=lambda n: getattr(n, self.time_attr))
|
||||
# the root node is not in the lru list
|
||||
assert len(nodes) == (
|
||||
total_lru + (0 if self.mamba else 1)
|
||||
), f"len(nodes): {len(nodes)}, total_lru: {total_lru}"
|
||||
|
||||
x_lru = self._get_lru()
|
||||
while len(nodes):
|
||||
x = heapq.heappop(nodes)
|
||||
for x in nodes:
|
||||
if x == tree_cache.root_node:
|
||||
# root node is not in the lru list
|
||||
continue
|
||||
@@ -393,7 +401,7 @@ class LRUList:
|
||||
|
||||
assert (
|
||||
x == x_lru
|
||||
), f"Incorrect LRU list, {self.mamba=}, x: {x.id=} != x_lru: {x_lru.id=}, {x.last_access_time=}, {x_lru.last_access_time=}"
|
||||
), f"Incorrect LRU list, {self.mamba=}, x: {x.id=} != x_lru: {x_lru.id=}, {getattr(x, self.time_attr)=}, {getattr(x_lru, self.time_attr)=}"
|
||||
assert (
|
||||
x_lru.full_lock_ref == 0
|
||||
), f"x_lru should not be locked when idle, {x_lru.full_lock_ref=}, {x_lru.id=}"
|
||||
@@ -1104,11 +1112,16 @@ class MambaRadixCache(KVCacheEventMixin, BasePrefixCache):
|
||||
cow_mamba = params.cow_mamba
|
||||
req = params.req
|
||||
|
||||
# update time for matched nodes, and make nodes closer to root to be least recently used
|
||||
# this allows mamba to evict nodes closer to root first
|
||||
# Full KV of the whole matched path is reused as prefix, so refresh the entire
|
||||
# chain (nodes closer to root end up least recently used, evicted first).
|
||||
node_update = last_node
|
||||
self.full_lru_list.reset_node_and_parents_mru(node_update, self.root_node)
|
||||
self.mamba_lru_list.reset_node_and_parents_mru(node_update, self.root_node)
|
||||
# Mamba only consumes last_node's state (cf. inc_lock_ref, which locks just this
|
||||
# node's mamba_value). Refreshing ancestors would keep a whole session's states
|
||||
# adjacent in the mamba LRU and evict cold sessions wholesale; touch only the used
|
||||
# state so older leaves survive.
|
||||
if last_node is not self.root_node and last_node.mamba_value is not None:
|
||||
self.mamba_lru_list.reset_node_mru(last_node)
|
||||
|
||||
# This last_access_time is for sanity check, can be deleted after validation in production
|
||||
cur_time = get_last_access_time()
|
||||
@@ -1170,12 +1183,13 @@ class MambaRadixCache(KVCacheEventMixin, BasePrefixCache):
|
||||
new_node.key = child.key[:split_len]
|
||||
new_node.value = child.value[:split_len].clone()
|
||||
|
||||
# child time should be later than parent's time for mamba tombstone
|
||||
# child time should be later than the new parent's time in the full LRU
|
||||
child.last_access_time = get_last_access_time()
|
||||
|
||||
# A split does not change the set of live mamba states (child keeps its value,
|
||||
# new_node is a mamba tombstone), so the mamba LRU is left untouched — only the
|
||||
# full LRU reorders around the new intermediate node.
|
||||
self.full_lru_list.remove_node(child)
|
||||
if child.mamba_value is not None:
|
||||
self.mamba_lru_list.remove_node(child)
|
||||
child.parent = new_node
|
||||
child.key = child.key[split_len:]
|
||||
child.value = child.value[split_len:].clone()
|
||||
@@ -1184,12 +1198,10 @@ class MambaRadixCache(KVCacheEventMixin, BasePrefixCache):
|
||||
child.hash_value, split_len, self.page_size
|
||||
)
|
||||
|
||||
# insert the new node and child into the lru lists, insert
|
||||
# insert the new node and child into the full lru list, insert
|
||||
# parent first so that parent is after child in the lru list
|
||||
self.full_lru_list.insert_mru(new_node)
|
||||
self.full_lru_list.insert_mru(child)
|
||||
if child.mamba_value is not None:
|
||||
self.mamba_lru_list.insert_mru(child)
|
||||
return new_node
|
||||
|
||||
def _insert_helper(
|
||||
@@ -1201,14 +1213,14 @@ class MambaRadixCache(KVCacheEventMixin, BasePrefixCache):
|
||||
chunked: bool = False,
|
||||
prev_prefix_len: int = 0,
|
||||
) -> Tuple[int, bool]:
|
||||
# Update the last access time from root to leaf, so that
|
||||
# mamba will tombstone the node closer to root first
|
||||
# Refresh the full LRU from root to leaf (the whole path is reused as prefix).
|
||||
# The mamba states of these existing nodes were not recomputed this insert, so
|
||||
# the mamba LRU is left untouched here; only genuinely new mamba states (the new
|
||||
# leaf / a revived tombstone below) are inserted.
|
||||
assert mamba_value is not None, "Mamba value should not be None here."
|
||||
node.last_access_time = get_last_access_time()
|
||||
if node != self.root_node:
|
||||
self.full_lru_list.reset_node_mru(node)
|
||||
if node.mamba_value is not None:
|
||||
self.mamba_lru_list.reset_node_mru(node)
|
||||
if len(key) == 0:
|
||||
return 0, True
|
||||
|
||||
@@ -1219,8 +1231,6 @@ class MambaRadixCache(KVCacheEventMixin, BasePrefixCache):
|
||||
node = node.children[child_key]
|
||||
node.last_access_time = get_last_access_time()
|
||||
self.full_lru_list.reset_node_mru(node)
|
||||
if node.mamba_value is not None:
|
||||
self.mamba_lru_list.reset_node_mru(node)
|
||||
prefix_len = node.key.match(key, page_size=self.page_size)
|
||||
|
||||
if prev_prefix_len < total_prefix_length + prefix_len:
|
||||
@@ -1260,7 +1270,6 @@ class MambaRadixCache(KVCacheEventMixin, BasePrefixCache):
|
||||
else: # mamba value already exists
|
||||
mamba_value_exist = True
|
||||
self.full_lru_list.reset_node_mru(node)
|
||||
self.mamba_lru_list.reset_node_mru(node)
|
||||
node.last_access_time = get_last_access_time()
|
||||
|
||||
return total_prefix_length, mamba_value_exist
|
||||
|
||||
Reference in New Issue
Block a user