Fix streaming session with paged KV cache (SWA/MLA) (#20070)
Co-authored-by: Yilong Zhao <74357408+happierpig@users.noreply.github.com> Co-authored-by: Aurick Qiao <6137920+aurickq@users.noreply.github.com>
This commit is contained in:
co-authored by
Yilong Zhao
Aurick Qiao
parent
230fb55899
commit
36b557d2c9
@@ -902,7 +902,10 @@ class Req(ReqDllmMixin):
|
|||||||
match_result.host_hit_length,
|
match_result.host_hit_length,
|
||||||
match_result.mamba_branching_seqlen,
|
match_result.mamba_branching_seqlen,
|
||||||
)
|
)
|
||||||
self.cache_protected_len = len(self.prefix_indices)
|
if match_result.cache_protected_len is not None:
|
||||||
|
self.cache_protected_len = match_result.cache_protected_len
|
||||||
|
else:
|
||||||
|
self.cache_protected_len = len(self.prefix_indices)
|
||||||
|
|
||||||
if self.is_dllm():
|
if self.is_dllm():
|
||||||
self._update_block_offset_for_dllm()
|
self._update_block_offset_for_dllm()
|
||||||
|
|||||||
@@ -26,6 +26,16 @@ class SchedulerRuntimeCheckerMixin:
|
|||||||
return self.tree_cache.session_held_tokens()
|
return self.tree_cache.session_held_tokens()
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
def _session_held_full_tokens(self: Scheduler) -> int:
|
||||||
|
if isinstance(self.tree_cache, SessionAwareCache):
|
||||||
|
return self.tree_cache.session_held_full_tokens()
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def _session_held_swa_tokens(self: Scheduler) -> int:
|
||||||
|
if isinstance(self.tree_cache, SessionAwareCache):
|
||||||
|
return self.tree_cache.session_held_swa_tokens()
|
||||||
|
return 0
|
||||||
|
|
||||||
def _session_held_req_count(self: Scheduler) -> int:
|
def _session_held_req_count(self: Scheduler) -> int:
|
||||||
if isinstance(self.tree_cache, SessionAwareCache):
|
if isinstance(self.tree_cache, SessionAwareCache):
|
||||||
return self.tree_cache.session_held_req_count()
|
return self.tree_cache.session_held_req_count()
|
||||||
@@ -104,11 +114,20 @@ class SchedulerRuntimeCheckerMixin:
|
|||||||
swa_available_size,
|
swa_available_size,
|
||||||
swa_evictable_size,
|
swa_evictable_size,
|
||||||
) = self._get_swa_token_info()
|
) = self._get_swa_token_info()
|
||||||
session_held = self._session_held_tokens()
|
session_held_full = self._session_held_full_tokens()
|
||||||
memory_leak = (full_num_used - session_held) != 0 or swa_num_used != 0
|
session_held_swa = self._session_held_swa_tokens()
|
||||||
|
|
||||||
|
# Streaming sessions hold tree locks during idle, so tree-protected
|
||||||
|
# tokens must be accounted for alongside session-held tokens.
|
||||||
|
full_protected = self.tree_cache.full_protected_size()
|
||||||
|
swa_protected = self.tree_cache.swa_protected_size()
|
||||||
|
full_leaked = full_num_used - full_protected - session_held_full
|
||||||
|
swa_leaked = swa_num_used - swa_protected - session_held_swa
|
||||||
|
memory_leak = full_leaked != 0 or swa_leaked != 0
|
||||||
token_msg = (
|
token_msg = (
|
||||||
f"{self.full_tokens_per_layer=}, {full_available_size=}, {full_evictable_size=}, {self.tree_cache.full_protected_size()=}\n"
|
f"{full_leaked=}, {swa_leaked=}\n"
|
||||||
f"{self.swa_tokens_per_layer=}, {swa_available_size=}, {swa_evictable_size=}, {self.tree_cache.swa_protected_size()=}, {session_held=}\n"
|
f"{self.full_tokens_per_layer=}, {full_available_size=}, {full_evictable_size=}, {full_protected=}, {session_held_full=}\n"
|
||||||
|
f"{self.swa_tokens_per_layer=}, {swa_available_size=}, {swa_evictable_size=}, {swa_protected=}, {session_held_swa=}\n"
|
||||||
)
|
)
|
||||||
return memory_leak, token_msg
|
return memory_leak, token_msg
|
||||||
|
|
||||||
|
|||||||
@@ -111,6 +111,7 @@ class MatchResult(NamedTuple):
|
|||||||
last_host_backup_node: Any = None
|
last_host_backup_node: Any = None
|
||||||
host_hit_length: int = 0
|
host_hit_length: int = 0
|
||||||
mamba_branching_seqlen: Optional[int] = None
|
mamba_branching_seqlen: Optional[int] = None
|
||||||
|
cache_protected_len: Optional[int] = None
|
||||||
|
|
||||||
|
|
||||||
class BasePrefixCache(ABC, PrefixCacheTrait):
|
class BasePrefixCache(ABC, PrefixCacheTrait):
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ from sglang.srt.mem_cache.base_prefix_cache import (
|
|||||||
MatchPrefixParams,
|
MatchPrefixParams,
|
||||||
MatchResult,
|
MatchResult,
|
||||||
)
|
)
|
||||||
|
from sglang.srt.utils.common import ceil_align
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from sglang.srt.managers.schedule_batch import Req
|
from sglang.srt.managers.schedule_batch import Req
|
||||||
@@ -178,6 +179,7 @@ class SessionAwareCache(BasePrefixCache):
|
|||||||
device_indices=device_indices,
|
device_indices=device_indices,
|
||||||
last_device_node=slot.virtual_node,
|
last_device_node=slot.virtual_node,
|
||||||
last_host_node=slot.virtual_node,
|
last_host_node=slot.virtual_node,
|
||||||
|
cache_protected_len=slot.cache_protected_len,
|
||||||
)
|
)
|
||||||
|
|
||||||
def cache_finished_req(self, req: Req, is_insert: bool = True, **kwargs):
|
def cache_finished_req(self, req: Req, is_insert: bool = True, **kwargs):
|
||||||
@@ -242,7 +244,23 @@ class SessionAwareCache(BasePrefixCache):
|
|||||||
total = 0
|
total = 0
|
||||||
for slot in self.slots.values():
|
for slot in self.slots.values():
|
||||||
if slot.req_pool_idx is not None:
|
if slot.req_pool_idx is not None:
|
||||||
total += slot.kv_allocated_len - slot.cache_protected_len
|
allocated = ceil_align(slot.kv_allocated_len, self.page_size)
|
||||||
|
total += allocated - slot.cache_protected_len
|
||||||
|
return total
|
||||||
|
|
||||||
|
def session_held_full_tokens(self) -> int:
|
||||||
|
"""An alias to align the naming style of SWA"""
|
||||||
|
return self.session_held_tokens()
|
||||||
|
|
||||||
|
def session_held_swa_tokens(self) -> int:
|
||||||
|
"""Total SWA tokens held by session slots, not tracked by the tree."""
|
||||||
|
total = 0
|
||||||
|
for slot in self.slots.values():
|
||||||
|
if slot.req_pool_idx is not None:
|
||||||
|
allocated = ceil_align(slot.kv_allocated_len, self.page_size)
|
||||||
|
total += allocated - max(
|
||||||
|
slot.cache_protected_len, slot.swa_evicted_seqlen
|
||||||
|
)
|
||||||
return total
|
return total
|
||||||
|
|
||||||
def session_held_req_count(self) -> int:
|
def session_held_req_count(self) -> int:
|
||||||
@@ -305,7 +323,14 @@ class SessionAwareCache(BasePrefixCache):
|
|||||||
def init_metrics_collector(self):
|
def init_metrics_collector(self):
|
||||||
return self.inner.init_metrics_collector()
|
return self.inner.init_metrics_collector()
|
||||||
|
|
||||||
# Forward attribute access for cache-specific methods (e.g. sanity_check,
|
def sanity_check(self):
|
||||||
|
# Skip inner sanity check when sessions hold tree locks, because
|
||||||
|
# the check asserts all nodes are unlocked during idle.
|
||||||
|
if any(s.req_pool_idx is not None for s in self.slots.values()):
|
||||||
|
return
|
||||||
|
self.inner.sanity_check()
|
||||||
|
|
||||||
|
# Forward attribute access for cache-specific methods (e.g.
|
||||||
# sliding_window_size, all_values_flatten, etc.)
|
# sliding_window_size, all_values_flatten, etc.)
|
||||||
def __getattr__(self, name):
|
def __getattr__(self, name):
|
||||||
return getattr(self.inner, name)
|
return getattr(self.inner, name)
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ from sglang.srt.utils import kill_process_tree
|
|||||||
from sglang.srt.utils.hf_transformers_utils import get_tokenizer
|
from sglang.srt.utils.hf_transformers_utils import get_tokenizer
|
||||||
from sglang.test.ci.ci_register import register_cuda_ci
|
from sglang.test.ci.ci_register import register_cuda_ci
|
||||||
from sglang.test.test_utils import (
|
from sglang.test.test_utils import (
|
||||||
DEFAULT_MODEL_NAME_FOR_TEST,
|
|
||||||
DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH,
|
DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH,
|
||||||
DEFAULT_URL_FOR_TEST,
|
DEFAULT_URL_FOR_TEST,
|
||||||
CustomTestCase,
|
CustomTestCase,
|
||||||
@@ -318,19 +317,25 @@ def _print_summary(all_results: Dict[str, List[ModeResult]]):
|
|||||||
|
|
||||||
|
|
||||||
class TestSessionLatency(CustomTestCase):
|
class TestSessionLatency(CustomTestCase):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpClass(cls):
|
def setUpClass(cls):
|
||||||
cls.model = DEFAULT_MODEL_NAME_FOR_TEST
|
cls.model = "openai/gpt-oss-20b"
|
||||||
cls.base_url = DEFAULT_URL_FOR_TEST
|
cls.base_url = DEFAULT_URL_FOR_TEST
|
||||||
|
# NOTE: Overlap scheduling commits KV cache one step ahead,
|
||||||
|
# so the last decode token is cached (unlike non-overlap).
|
||||||
|
# Disable overlap to keep session cache behavior consistent.
|
||||||
cls.process = popen_launch_server(
|
cls.process = popen_launch_server(
|
||||||
cls.model,
|
cls.model,
|
||||||
cls.base_url,
|
cls.base_url,
|
||||||
timeout=DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH,
|
timeout=DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH,
|
||||||
other_args=[
|
other_args=[
|
||||||
"--attention-backend",
|
"--disable-overlap-schedule",
|
||||||
"flashinfer",
|
|
||||||
"--enable-streaming-session",
|
"--enable-streaming-session",
|
||||||
|
"--mem-fraction-static",
|
||||||
|
"0.70",
|
||||||
|
"--disable-piecewise-cuda-graph",
|
||||||
|
"--page-size",
|
||||||
|
"4",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
cls.tokenizer = get_tokenizer(cls.model)
|
cls.tokenizer = get_tokenizer(cls.model)
|
||||||
|
|||||||
Reference in New Issue
Block a user