59a0a3844c
PR Test AMD / cancel-on-close (push) Has been skipped
PR Test NVIDIA ARM / scan (push) Has been skipped
PR Test NVIDIA / cancel-on-close (push) Has been skipped
PR Test AMD / scan (push) Has been skipped
PR Test NVIDIA ARM / cancel-on-close (push) Has been skipped
PR Test NVIDIA / scan (push) Has been skipped
Release Docker Images / build (cu129-torch-2.11.0) (push) Has been skipped
Release Docker Images / build (cu130-torch-2.11.0) (push) Has been skipped
Release PyPI / publish (push) Has been skipped
Scheduler Python Test / test (push) Successful in 27m19s
Docs / build (push) Successful in 28m8s
Scheduler C++ Test / test (push) Successful in 28m19s
Scheduler C++ Test / test-flat (push) Successful in 28m18s
Docs / deploy (push) Has been cancelled
PR Test AMD / finish (push) Has been cancelled
PR Test NVIDIA / finish (push) Has been cancelled
PR Test NVIDIA ARM / finish (push) Has been cancelled
PR Test NVIDIA ARM / ${{ matrix.name }} (${{ matrix.runner }}) (push) Has been cancelled
PR Test AMD / ${{ matrix.name }} (${{ matrix.runner }}) (push) Has been cancelled
PR Test NVIDIA / ${{ matrix.name }} (${{ matrix.runner }}) (push) Has been cancelled
648 lines
23 KiB
Python
Executable File
648 lines
23 KiB
Python
Executable File
# Copyright (c) 2026 LightSeek Foundation
|
|
#
|
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
# of this software and associated documentation files (the "Software"), to deal
|
|
# in the Software without restriction, including without limitation the rights
|
|
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
# copies of the Software, and to permit persons to whom the Software is
|
|
# furnished to do so, subject to the following conditions:
|
|
#
|
|
# The above copyright notice and this permission notice shall be included in
|
|
# all copies or substantial portions of the Software.
|
|
#
|
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
# SOFTWARE.
|
|
|
|
"""
|
|
The prefix tree data structure for managing the KV cache.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import dataclasses
|
|
import heapq
|
|
import time
|
|
from collections import defaultdict
|
|
from collections.abc import Callable
|
|
from typing import TYPE_CHECKING
|
|
|
|
import torch
|
|
|
|
from tokenspeed.runtime.cache.allocator import KVAllocator
|
|
from tokenspeed.runtime.cache.base_prefix_cache import BasePrefixCache, MatchResult
|
|
from tokenspeed.runtime.cache.evict_policy import (
|
|
EvictionStrategy,
|
|
FIFOStrategy,
|
|
FILOStrategy,
|
|
LFUStrategy,
|
|
LRUStrategy,
|
|
MRUStrategy,
|
|
PriorityStrategy,
|
|
)
|
|
from tokenspeed.runtime.cache.req_to_token_pool import ReqToTokenPool
|
|
from tokenspeed.runtime.layers.attention.kv_cache.base import BaseTokenToKVPool
|
|
from tokenspeed.runtime.utils import get_colorful_logger
|
|
|
|
logger = get_colorful_logger(__name__)
|
|
|
|
if TYPE_CHECKING:
|
|
from tokenspeed.runtime.engine.request import Req
|
|
|
|
# Import KV events classes if needed
|
|
try:
|
|
from tokenspeed.runtime.pd.kv_events import (
|
|
AllBlocksCleared,
|
|
BlockRemoved,
|
|
BlockStored,
|
|
)
|
|
|
|
KV_EVENTS_AVAILABLE = True
|
|
except ImportError:
|
|
BlockStored = None
|
|
BlockRemoved = None
|
|
AllBlocksCleared = None
|
|
KV_EVENTS_AVAILABLE = False
|
|
logger.warning(
|
|
"KV Events module not available. "
|
|
"KV cache events will not be emitted even if enable_kv_cache_events=True."
|
|
)
|
|
|
|
|
|
@dataclasses.dataclass
|
|
class CacheInitParams:
|
|
disable: bool
|
|
req_to_token_pool: ReqToTokenPool
|
|
token_to_kv_pool_allocator: KVAllocator
|
|
page_size: int
|
|
|
|
token_to_kv_pool: BaseTokenToKVPool = None
|
|
tp_cache_group: torch.distributed.ProcessGroup | None = None
|
|
eviction_policy: str = "lru"
|
|
disable_finished_insert: bool = False
|
|
|
|
enable_metrics: bool = False
|
|
enable_kv_cache_events: bool = False
|
|
|
|
|
|
class TreeNode:
|
|
counter = 0
|
|
|
|
def __init__(self, id: int | None = None, priority: int = 0):
|
|
self.children = defaultdict(TreeNode)
|
|
self.parent = None
|
|
self.key = None
|
|
self.value = None
|
|
self.lock_ref = 0
|
|
self.last_access_time = time.monotonic()
|
|
self.creation_time = time.monotonic()
|
|
|
|
self.id = TreeNode.counter if id is None else id
|
|
TreeNode.counter += 1
|
|
|
|
##### KVStore #####
|
|
# host_value is token level, others are page level
|
|
self.hit_count = 0
|
|
# indicating the node is locked to protect from eviction
|
|
# incremented when the node is referenced by a storage operation
|
|
self.host_ref_counter = 0
|
|
# store the host indices of KV cache
|
|
self.host_value: torch.Tensor | None = None
|
|
# store hash values of each pages
|
|
self.hash_value: list[str] | None = None
|
|
# priority for priority-aware eviction
|
|
self.priority = priority
|
|
|
|
@property
|
|
def evicted(self):
|
|
return self.value is None
|
|
|
|
def __lt__(self, other: "TreeNode"):
|
|
return self.last_access_time < other.last_access_time
|
|
|
|
|
|
def _key_match(key0: list, key1: list):
|
|
i = 0
|
|
for k0, k1 in zip(key0, key1):
|
|
if k0 != k1:
|
|
break
|
|
i += 1
|
|
return i
|
|
|
|
|
|
class PrefixCache(BasePrefixCache):
|
|
def __init__(self, params: CacheInitParams):
|
|
self.disable = params.disable
|
|
self.req_to_token_pool = params.req_to_token_pool
|
|
self.token_to_kv_pool_allocator = params.token_to_kv_pool_allocator
|
|
self.enable_kv_cache_events = params.enable_kv_cache_events
|
|
self.page_size = (
|
|
1
|
|
if params.token_to_kv_pool_allocator is None
|
|
else params.token_to_kv_pool_allocator.page_size
|
|
)
|
|
self.eviction_policy = params.eviction_policy.lower()
|
|
self.key_match_fn = _key_match
|
|
|
|
strategy_types: dict[str, type[EvictionStrategy]] = {
|
|
"lru": LRUStrategy,
|
|
"lfu": LFUStrategy,
|
|
"fifo": FIFOStrategy,
|
|
"mru": MRUStrategy,
|
|
"filo": FILOStrategy,
|
|
"priority": PriorityStrategy,
|
|
}
|
|
strategy_type = strategy_types.get(self.eviction_policy)
|
|
if strategy_type is None:
|
|
raise ValueError(
|
|
f"Unknown eviction policy: {self.eviction_policy}. Supported policies: 'lru', 'lfu', 'fifo', 'mru', 'filo', 'priority'."
|
|
)
|
|
self.eviction_strategy = strategy_type()
|
|
# Initialize KV event queue
|
|
self.kv_event_queue = []
|
|
|
|
# Warn if KV events are enabled but module is not available
|
|
if self.enable_kv_cache_events and not KV_EVENTS_AVAILABLE:
|
|
logger.warning(
|
|
"KV cache events are enabled (enable_kv_cache_events=True) "
|
|
"but KV Events module is not available. Events will not be emitted."
|
|
)
|
|
|
|
self.reset()
|
|
|
|
##### Public API #####
|
|
|
|
def reset(self):
|
|
self.root_node = TreeNode()
|
|
self.root_node.key = []
|
|
self.root_node.value = []
|
|
self.root_node.host_value = []
|
|
self.root_node.lock_ref = 1
|
|
self.root_node.hash_value = []
|
|
self.evictable_size_ = 0
|
|
self.protected_size_ = 0
|
|
self.evictable_leaves: set[TreeNode] = set()
|
|
|
|
if self.enable_kv_cache_events and KV_EVENTS_AVAILABLE:
|
|
self.kv_event_queue.append(AllBlocksCleared())
|
|
|
|
def match_prefix(self, key: list, **kwargs) -> MatchResult:
|
|
"""Find the matching prefix from the prefix tree.
|
|
Args:
|
|
key: A list of token IDs to find a matching prefix.
|
|
Returns:
|
|
A tuple of a tensor of matching prefix token IDs and
|
|
the last node that contains the prefix values. Note that
|
|
this API can modify the internal state of the prefix tree.
|
|
The last node creates a new child if the prefix is shorter
|
|
than the last node's value.
|
|
"""
|
|
|
|
if self.disable or len(key) == 0:
|
|
return self._empty_match_result()
|
|
|
|
page_size = self.token_to_kv_pool_allocator.page_size
|
|
# Compatible with whether the incoming key is paged
|
|
if not isinstance(key[0], tuple):
|
|
full_page_num = len(key) // page_size
|
|
paged_token_ids = [
|
|
tuple(key[i * page_size : (i + 1) * page_size])
|
|
for i in range(0, full_page_num)
|
|
]
|
|
else:
|
|
paged_token_ids = key
|
|
if len(paged_token_ids) == 0:
|
|
return self._empty_match_result()
|
|
value = []
|
|
last_node = [self.root_node]
|
|
self._match_prefix_helper(self.root_node, paged_token_ids, value, last_node)
|
|
if value and isinstance(value[0], list):
|
|
flat_value = [e for arr in value for e in arr]
|
|
value = torch.concat(flat_value)
|
|
elif value:
|
|
value = torch.concat(value)
|
|
else:
|
|
value = torch.tensor([], dtype=torch.int32)
|
|
return MatchResult(
|
|
device_indices=value,
|
|
last_device_node=last_node[0],
|
|
last_host_node=last_node[0],
|
|
device_prefix_length=len(value) * page_size,
|
|
)
|
|
|
|
def insert(self, key: list, value=None):
|
|
if self.disable:
|
|
return 0
|
|
|
|
if value is None:
|
|
value = [x for x in key]
|
|
return self._insert_helper(self.root_node, key, value)
|
|
|
|
def cache_finished_req(
|
|
self,
|
|
req: Req,
|
|
token_ids: list[int] | None = None,
|
|
delay_req_pool_release=False,
|
|
):
|
|
"""Cache request when it finishes."""
|
|
if self.disable:
|
|
alloced_len = self.req_to_token_pool.alloced_lens[req.req_pool_idx].item()
|
|
self.token_to_kv_pool_allocator.free_req_cache(
|
|
req.req_pool_idx, alloced_len
|
|
)
|
|
if not delay_req_pool_release:
|
|
self.req_to_token_pool.free(req.req_pool_idx)
|
|
req.req_pool_idx = None
|
|
return
|
|
|
|
if token_ids is None:
|
|
token_ids = (req.origin_input_ids + req.output_ids)[:-1]
|
|
# Prefix Cache takes one ref in memory pool
|
|
req_pool_idx = req.req_pool_idx
|
|
page_size = self.token_to_kv_pool_allocator.page_size
|
|
seq_len = len(token_ids)
|
|
# without last not full page
|
|
full_page_num = seq_len // page_size
|
|
paged_token_ids = [
|
|
tuple(token_ids[i * page_size : (i + 1) * page_size])
|
|
for i in range(0, full_page_num)
|
|
]
|
|
page_ids = self.token_to_kv_pool_allocator.req_to_page[
|
|
req_pool_idx, :full_page_num
|
|
].clone()
|
|
logger.debug("insert_page_ids=%s", page_ids)
|
|
_ = self.insert(paged_token_ids, page_ids)
|
|
|
|
# After insert, the tree changed so the match prefix ids changed
|
|
new_prefix_page_ids = self.match_prefix(paged_token_ids).device_indices
|
|
# new_prefix_page_ids is cached in tree, free the diff part in page_ids
|
|
|
|
if new_prefix_page_ids.numel() > 0:
|
|
self.token_to_kv_pool_allocator.free_with_diff(
|
|
new_prefix_page_ids, page_ids
|
|
)
|
|
self.token_to_kv_pool_allocator.free_extra_pages_not_cached(
|
|
req_pool_idx,
|
|
seq_len,
|
|
self.req_to_token_pool.alloced_lens[req_pool_idx].item(),
|
|
)
|
|
if not delay_req_pool_release:
|
|
self.req_to_token_pool.free(req.req_pool_idx)
|
|
req.req_pool_idx = None
|
|
# Remove req slot release the cache lock
|
|
if req.last_node is not None:
|
|
self.dec_lock_ref(req.last_node)
|
|
req.last_node = None
|
|
logger.debug(
|
|
"[cache_finished_req]\nold_prefix_page_ids=%s\nnew_prefix_page_ids=%s\nlen(token_ids)=%s \nalloced_lens=%s, self.evictable_size_=%r, self.protected_size_=%r, req.last_node=%r",
|
|
self.token_to_kv_pool_allocator.req_to_page[
|
|
req_pool_idx, : (seq_len + page_size - 1) // page_size
|
|
].tolist(),
|
|
new_prefix_page_ids.tolist(),
|
|
len(token_ids),
|
|
self.req_to_token_pool.alloced_lens[req_pool_idx],
|
|
self.evictable_size_,
|
|
self.protected_size_,
|
|
req.last_node,
|
|
)
|
|
|
|
def cache_unfinished_req(self, req: Req, token_ids: list[int] | None = None):
|
|
"""Cache request when it is unfinished."""
|
|
|
|
if self.disable:
|
|
return
|
|
|
|
if token_ids is None:
|
|
token_ids = req.fill_ids
|
|
|
|
page_size = self.token_to_kv_pool_allocator.page_size
|
|
req_pool_idx = req.req_pool_idx
|
|
seq_len = len(token_ids)
|
|
# without last not full page
|
|
full_page_num = seq_len // page_size
|
|
paged_token_ids = [
|
|
tuple(token_ids[i * page_size : (i + 1) * page_size])
|
|
for i in range(0, full_page_num)
|
|
]
|
|
|
|
page_ids = self.token_to_kv_pool_allocator.req_to_page[
|
|
req_pool_idx, :full_page_num
|
|
].clone()
|
|
_ = self.insert(paged_token_ids, page_ids)
|
|
# After insert, perform matching, use page_id in prefix tree to replace the allocated
|
|
# page before insert, release diff part, and write to req_to_token_pool
|
|
match_result = self.match_prefix(paged_token_ids)
|
|
new_prefix_page_ids, new_last_node = (
|
|
match_result.device_indices,
|
|
match_result.last_device_node,
|
|
)
|
|
if new_prefix_page_ids.numel() > 0:
|
|
diff = self.token_to_kv_pool_allocator.free_with_diff(
|
|
new_prefix_page_ids, page_ids
|
|
)
|
|
diff_idxs = torch.nonzero(diff)
|
|
self.token_to_kv_pool_allocator.req_to_page[
|
|
req.req_pool_idx, diff_idxs.squeeze()
|
|
] = new_prefix_page_ids[diff]
|
|
# Fix requires prefix cache to store CPU page IDs alongside GPU ones.
|
|
self.token_to_kv_pool_allocator.req_to_page_cpu[
|
|
req.req_pool_idx, diff_idxs.squeeze().cpu()
|
|
] = new_prefix_page_ids[diff].cpu()
|
|
token_level_offsets = torch.arange(
|
|
self.page_size, device=self.req_to_token_pool.device
|
|
)
|
|
indices_start_locs = diff_idxs * self.page_size
|
|
diff_slots_indices = (
|
|
indices_start_locs[:, None] + token_level_offsets
|
|
).flatten()
|
|
new_slots_start_locs = new_prefix_page_ids[diff] * self.page_size
|
|
new_slots = (new_slots_start_locs[:, None] + token_level_offsets).flatten()
|
|
self.req_to_token_pool.req_to_token[
|
|
req.req_pool_idx, diff_slots_indices
|
|
] = new_slots.to(torch.int32)
|
|
self.dec_lock_ref(req.last_node)
|
|
self.inc_lock_ref(new_last_node)
|
|
req.last_node = new_last_node
|
|
|
|
def pretty_print(self, start_str: str = ""):
|
|
logger.debug(
|
|
self._print_helper(
|
|
self.root_node, 0, start_str + f" #tokens: {self.total_size()} "
|
|
)
|
|
)
|
|
|
|
def total_size(self):
|
|
return self._total_size_helper(self.root_node)
|
|
|
|
def evict(self, num_tokens: int, evict_callback: Callable = None):
|
|
if self.disable:
|
|
return
|
|
|
|
heap = [
|
|
(self.eviction_strategy.get_priority(node), node)
|
|
for node in self.evictable_leaves
|
|
]
|
|
heapq.heapify(heap)
|
|
|
|
num_evicted = 0
|
|
while num_evicted < num_tokens and heap:
|
|
_, x = heapq.heappop(heap)
|
|
|
|
# evictable_leaves only contains unlocked leaves so lock_ref > 0 can
|
|
# only happen for cascade parents pushed mid-loop; guard defensively.
|
|
if x.lock_ref > 0:
|
|
continue
|
|
|
|
self.token_to_kv_pool_allocator.append_to_later_free(x.value)
|
|
num_evicted += len(x.value)
|
|
self._delete_leaf(x) # removes x from evictable_leaves, may add parent
|
|
|
|
# Push cascade parent onto the working heap so it can be evicted in
|
|
# this same call. _delete_leaf already added it to evictable_leaves.
|
|
parent = x.parent
|
|
if (
|
|
not parent.children
|
|
and parent.lock_ref == 0
|
|
and parent != self.root_node
|
|
):
|
|
heapq.heappush(
|
|
heap, (self.eviction_strategy.get_priority(parent), parent)
|
|
)
|
|
|
|
self.token_to_kv_pool_allocator.free_group_end()
|
|
return num_evicted
|
|
|
|
def inc_lock_ref(self, node: TreeNode):
|
|
if self.disable:
|
|
return 0
|
|
|
|
delta = 0
|
|
while node != self.root_node:
|
|
if node.lock_ref == 0:
|
|
self.evictable_size_ -= len(node.value)
|
|
self.protected_size_ += len(node.value)
|
|
delta -= len(node.value)
|
|
self.evictable_leaves.discard(node)
|
|
node.lock_ref += 1
|
|
node = node.parent
|
|
return delta
|
|
|
|
def dec_lock_ref(self, node: TreeNode):
|
|
if self.disable:
|
|
return 0
|
|
|
|
if node is None:
|
|
return 0
|
|
|
|
delta = 0
|
|
while node != self.root_node:
|
|
if node.lock_ref == 1:
|
|
self.evictable_size_ += len(node.value)
|
|
self.protected_size_ -= len(node.value)
|
|
delta += len(node.value)
|
|
if not node.children:
|
|
self.evictable_leaves.add(node)
|
|
node.lock_ref -= 1
|
|
node = node.parent
|
|
return delta
|
|
|
|
def evictable_size(self):
|
|
return self.evictable_size_
|
|
|
|
def protected_size(self):
|
|
# protected size refers to the size of the cache that is locked
|
|
return self.protected_size_
|
|
|
|
##### Internal Helper Functions #####
|
|
|
|
def _update_leaf_status(self, node: TreeNode) -> None:
|
|
if node == self.root_node:
|
|
return
|
|
if not node.children and node.lock_ref == 0:
|
|
self.evictable_leaves.add(node)
|
|
else:
|
|
self.evictable_leaves.discard(node)
|
|
|
|
def _empty_match_result(self):
|
|
return MatchResult(
|
|
device_indices=torch.tensor([], dtype=torch.int32),
|
|
device_prefix_length=0,
|
|
host_hit_length=0,
|
|
last_device_node=self.root_node,
|
|
last_host_node=self.root_node,
|
|
)
|
|
|
|
def _match_prefix_helper(
|
|
self, node: TreeNode, key: list, value, last_node: TreeNode
|
|
):
|
|
node.last_access_time = time.monotonic()
|
|
if not key:
|
|
return
|
|
|
|
if key[0] in node.children:
|
|
child = node.children[key[0]]
|
|
prefix_len = _key_match(child.key, key)
|
|
if prefix_len < len(child.key):
|
|
new_node = self._split_node(child.key, child, prefix_len)
|
|
value.append(new_node.value)
|
|
last_node[0] = new_node
|
|
else:
|
|
value.append(child.value)
|
|
last_node[0] = child
|
|
self._match_prefix_helper(child, key[prefix_len:], value, last_node)
|
|
|
|
def _split_node(self, key, child: TreeNode, split_len: int):
|
|
# new_node -> child
|
|
new_node = TreeNode()
|
|
new_node.children = {key[split_len]: child}
|
|
new_node.parent = child.parent
|
|
new_node.lock_ref = child.lock_ref
|
|
new_node.key = child.key[:split_len]
|
|
new_node.value = child.value[:split_len]
|
|
child.parent = new_node
|
|
child.key = child.key[split_len:]
|
|
child.value = child.value[split_len:]
|
|
new_node.parent.children[key[0]] = new_node
|
|
return new_node
|
|
|
|
def _insert_helper(self, node: TreeNode, key: list, value):
|
|
node.last_access_time = time.monotonic()
|
|
if not key:
|
|
return 0
|
|
|
|
if key[0] in node.children:
|
|
child = node.children[key[0]]
|
|
prefix_len = _key_match(child.key, key)
|
|
|
|
if prefix_len == len(child.key):
|
|
if prefix_len == len(key):
|
|
return prefix_len
|
|
else:
|
|
key = key[prefix_len:]
|
|
value = value[prefix_len:]
|
|
return prefix_len + self._insert_helper(child, key, value)
|
|
|
|
new_node = self._split_node(child.key, child, prefix_len)
|
|
return prefix_len + self._insert_helper(
|
|
new_node, key[prefix_len:], value[prefix_len:]
|
|
)
|
|
|
|
if key:
|
|
new_node = TreeNode()
|
|
new_node.parent = node
|
|
new_node.key = key
|
|
new_node.value = value
|
|
node.children[key[0]] = new_node
|
|
self.evictable_size_ += len(value)
|
|
# New node is a leaf; parent may have transitioned from leaf to internal.
|
|
self._update_leaf_status(new_node)
|
|
self._update_leaf_status(node)
|
|
|
|
# Emit BlockStored event when new KV blocks are inserted
|
|
if self.enable_kv_cache_events:
|
|
self._record_store_event(new_node)
|
|
return 0
|
|
|
|
def _print_helper(self, node: TreeNode, indent: int, print_str: str) -> str:
|
|
for _, child in node.children.items():
|
|
print_str += "\n"
|
|
print_str += " " * indent
|
|
print_str += f"{child=} key_len={len(child.key)} "
|
|
print_str += f"value={child.value} {child.lock_ref=}"
|
|
print_str = self._print_helper(
|
|
child, indent=indent + 2, print_str=print_str
|
|
)
|
|
return print_str
|
|
|
|
def _delete_leaf(self, node):
|
|
del node.parent.children[node.key[0]]
|
|
self.evictable_size_ -= len(node.key)
|
|
self.evictable_leaves.discard(node)
|
|
# Parent may have become a childless leaf.
|
|
self._update_leaf_status(node.parent)
|
|
|
|
# Emit BlockRemoved event when KV blocks are removed
|
|
if self.enable_kv_cache_events:
|
|
self._record_remove_event(node)
|
|
|
|
def _total_size_helper(self, node: TreeNode):
|
|
if node.evicted:
|
|
return 0
|
|
x = len(node.value)
|
|
for child in node.children.values():
|
|
x += self._total_size_helper(child)
|
|
return x
|
|
|
|
def _collect_leaves(self):
|
|
ret_list = []
|
|
stack = [self.root_node]
|
|
|
|
while stack:
|
|
cur_node = stack.pop()
|
|
if not cur_node.children:
|
|
ret_list.append(cur_node)
|
|
else:
|
|
stack.extend(cur_node.children.values())
|
|
|
|
return ret_list
|
|
|
|
def _record_store_event(self, node: TreeNode):
|
|
"""Record BlockStored event for a node."""
|
|
if (
|
|
not self.enable_kv_cache_events
|
|
or not KV_EVENTS_AVAILABLE
|
|
or node.key is None
|
|
):
|
|
return
|
|
|
|
# TokenSpeed uses tuples as keys, where each tuple represents a page
|
|
# node.key is a list of tuples: [(token1, token2, ...), (token3, token4, ...), ...]
|
|
# Each tuple is already a page, so we iterate over them directly
|
|
|
|
parent_block_hash = None
|
|
if node.parent and node.parent != self.root_node and node.parent.key:
|
|
# Use the last page (tuple) of the parent
|
|
parent_block_hash = hash(node.parent.key[-1])
|
|
|
|
# Each element in node.key is already a page (tuple of tokens)
|
|
for page_tuple in node.key:
|
|
if not page_tuple:
|
|
continue
|
|
|
|
# Convert tuple to list for token_ids
|
|
token_ids = list(page_tuple)
|
|
block_hash = hash(page_tuple)
|
|
|
|
self.kv_event_queue.append(
|
|
BlockStored(
|
|
block_hashes=[block_hash],
|
|
parent_block_hash=parent_block_hash,
|
|
token_ids=token_ids,
|
|
block_size=len(token_ids),
|
|
lora_id=None,
|
|
)
|
|
)
|
|
# Chain next chunk to this one
|
|
parent_block_hash = block_hash
|
|
|
|
def _record_remove_event(self, node: TreeNode):
|
|
"""Record BlockRemoved event for a node."""
|
|
if (
|
|
not self.enable_kv_cache_events
|
|
or not KV_EVENTS_AVAILABLE
|
|
or node.key is None
|
|
):
|
|
return
|
|
|
|
# Create BlockRemoved event for each page
|
|
for start in range(0, len(node.key), self.page_size):
|
|
page_tokens = node.key[start : start + self.page_size]
|
|
if not page_tokens:
|
|
continue
|
|
block_hash = hash(tuple(page_tokens))
|
|
self.kv_event_queue.append(BlockRemoved(block_hashes=[block_hash]))
|