Scope prefetch cache state to the request attempt (#39318)
This commit is contained in:
@@ -12,6 +12,7 @@ import torch
|
||||
from sglang.srt.disaggregation.base import KVPoll
|
||||
from sglang.srt.managers.schedule_policy import match_prefix_for_req
|
||||
from sglang.srt.mem_cache.base_prefix_cache import (
|
||||
CacheRequestOutcome,
|
||||
InitLoadBackParams,
|
||||
)
|
||||
|
||||
@@ -129,7 +130,7 @@ class DecodeHiCachePreallocMixin:
|
||||
else None
|
||||
)
|
||||
self.tree_cache.prefetch_from_storage(
|
||||
req.rid,
|
||||
req.cache_request_handle,
|
||||
prefix_match.last_host_node,
|
||||
suffix,
|
||||
last_hash,
|
||||
@@ -137,8 +138,8 @@ class DecodeHiCachePreallocMixin:
|
||||
extra_key=req.extra_key,
|
||||
cache_salt=req.cache_salt,
|
||||
)
|
||||
prefix_match.prefetch_registered = (
|
||||
req.rid in self.tree_cache.ongoing_prefetch
|
||||
prefix_match.prefetch_registered = self.tree_cache.has_ongoing_prefetch(
|
||||
req.cache_request_handle
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
@@ -186,7 +187,9 @@ class DecodeHiCacheTransferMixin:
|
||||
decode_req.prefix_match is not None
|
||||
and decode_req.prefix_match.prefetch_registered
|
||||
):
|
||||
self.tree_cache.release_aborted_request(decode_req.req.rid)
|
||||
self.tree_cache.finish(
|
||||
decode_req.req.cache_request_handle, CacheRequestOutcome.ABORT
|
||||
)
|
||||
if decode_req.hicache_restored_node is not None:
|
||||
self.tree_cache.dec_lock_ref(
|
||||
decode_req.hicache_restored_node,
|
||||
@@ -207,9 +210,9 @@ class DecodeHiCacheTransferMixin:
|
||||
|
||||
# Wait for L3 -> L2 prefetch to drain (skip when no L3 hit).
|
||||
if pm.l3_storage_hit_length > 0:
|
||||
if not self.tree_cache.check_prefetch_progress(dr.req.rid):
|
||||
if not self.tree_cache.check_prefetch_progress(dr.req.cache_request_handle):
|
||||
return False
|
||||
self.tree_cache.pop_prefetch_loaded_tokens(dr.req.rid)
|
||||
self.tree_cache.pop_prefetch_loaded_tokens(dr.req.cache_request_handle)
|
||||
|
||||
# Re-match: req.last_node / prefix_indices updated to current device state.
|
||||
rematch = match_prefix_for_req(
|
||||
|
||||
@@ -66,6 +66,7 @@ from sglang.srt.managers.schedule_batch import (
|
||||
Req,
|
||||
ScheduleBatch,
|
||||
)
|
||||
from sglang.srt.mem_cache.base_prefix_cache import CacheRequestOutcome
|
||||
from sglang.srt.mem_cache.common import (
|
||||
kv_to_page_indices,
|
||||
kv_to_page_num,
|
||||
@@ -999,6 +1000,9 @@ class SchedulerDisaggregationPrefillMixin:
|
||||
if not isinstance(req.finished_reason, FINISH_ABORT):
|
||||
req.finished_reason = FINISH_LENGTH(length=0)
|
||||
release_kv_cache(req, self.tree_cache) # unlock the tree
|
||||
self.tree_cache.finish(
|
||||
req.cache_request_handle, CacheRequestOutcome.SUCCESS
|
||||
)
|
||||
# FIXME: clean up req's data in transfer engine
|
||||
req.disagg_kv_sender.clear()
|
||||
done_reqs.append(req)
|
||||
@@ -1070,6 +1074,7 @@ class SchedulerDisaggregationPrefillMixin:
|
||||
logger.warning(error_message)
|
||||
req.time_stats.trace_ctx.abort(abort_info={"reason": error_message})
|
||||
release_kv_cache(req, self.tree_cache) # unlock the tree
|
||||
self._release_aborted_request(req)
|
||||
if not isinstance(req.finished_reason, FINISH_ABORT):
|
||||
prepare_abort(
|
||||
req, error_message, status_code=HTTPStatus.INTERNAL_SERVER_ERROR
|
||||
@@ -1111,7 +1116,7 @@ class SchedulerDisaggregationPrefillMixin:
|
||||
maybe_release_metadata_buffer(req, self.req_to_metadata_buffer_idx_allocator)
|
||||
req.pending_bootstrap = False
|
||||
if self.enable_hicache_storage:
|
||||
self.tree_cache.release_aborted_request(req.rid)
|
||||
self.tree_cache.finish(req.cache_request_handle, CacheRequestOutcome.ABORT)
|
||||
if req.kv.holds_kv or req.kv.holds_mamba:
|
||||
release_kv_cache(req, self.tree_cache, is_insert=False)
|
||||
return True
|
||||
@@ -1143,7 +1148,7 @@ class SchedulerDisaggregationPrefillMixin:
|
||||
if self.metrics_reporter.enable_metrics:
|
||||
self.metrics_collector.increment_bootstrap_failed_reqs()
|
||||
if self.enable_hicache_storage:
|
||||
self.tree_cache.release_aborted_request(req.rid)
|
||||
self.tree_cache.finish(req.cache_request_handle, CacheRequestOutcome.ABORT)
|
||||
|
||||
def handle_pending_bootstrap(self: Scheduler, req: Req, poll: KVPoll) -> bool:
|
||||
"""Return True when bootstrap is finalized and KV transfer can proceed."""
|
||||
@@ -1466,6 +1471,7 @@ class SchedulerDisaggregationPrefillMixin:
|
||||
"""Release KV cache and requeue an optimistic prefill request."""
|
||||
max_attempts = get_disagg().optimistic_prefill_attempts
|
||||
maybe_cache_unfinished_req(req, self.tree_cache)
|
||||
self._release_aborted_request(req)
|
||||
release_kv_cache(req, self.tree_cache)
|
||||
req.reset_for_retract()
|
||||
req.output_ids = array("q")
|
||||
@@ -1478,6 +1484,7 @@ class SchedulerDisaggregationPrefillMixin:
|
||||
req.output_dsa_topk_indices = None
|
||||
req.pending_bootstrap = True
|
||||
req.time_stats.reset_prefill_retry_time()
|
||||
req.advance_cache_request_handle()
|
||||
if req.prefill_attempt_count >= max_attempts:
|
||||
logger.info(
|
||||
f"Req {req.rid} exhausted optimistic prefill attempts "
|
||||
|
||||
@@ -104,6 +104,7 @@ from sglang.srt.mem_cache.allocation_sizing import get_alloc_reserve_per_decode
|
||||
from sglang.srt.mem_cache.allocator import BaseTokenToKVPoolAllocator
|
||||
from sglang.srt.mem_cache.base_prefix_cache import (
|
||||
BasePrefixCache,
|
||||
CacheRequestHandle,
|
||||
DecLockRefParams,
|
||||
MatchPrefixParams,
|
||||
zero_match_result,
|
||||
@@ -979,6 +980,7 @@ class Req(ReqDllmMixin):
|
||||
):
|
||||
# Input and output info
|
||||
self.rid = rid
|
||||
self.cache_request_handle = CacheRequestHandle(rid=rid, attempt_id=0)
|
||||
self.origin_input_ids = origin_input_ids
|
||||
self.origin_input_ids_unpadded = (
|
||||
origin_input_ids_unpadded
|
||||
@@ -1335,6 +1337,12 @@ class Req(ReqDllmMixin):
|
||||
# Snapshot of the scheduler prefill-token counter taken at waiting_queue entry; used by HRRN aging.
|
||||
self.arrival_processed_tokens: int = 0
|
||||
|
||||
def advance_cache_request_handle(self) -> None:
|
||||
self.cache_request_handle = dataclasses.replace(
|
||||
self.cache_request_handle,
|
||||
attempt_id=self.cache_request_handle.attempt_id + 1,
|
||||
)
|
||||
|
||||
@property
|
||||
def seqlen(self) -> int:
|
||||
"""Get the current sequence length of the request."""
|
||||
|
||||
@@ -980,7 +980,9 @@ class PrefillAdder:
|
||||
if req.retracted_stain:
|
||||
# Retraction attribution is intentionally omitted for now; discard
|
||||
# its lifecycle state so a later abort cannot report it as a drop.
|
||||
self.tree_cache.discard_storage_prefetch_accounting(req.rid)
|
||||
self.tree_cache.discard_storage_prefetch_accounting(
|
||||
req.cache_request_handle
|
||||
)
|
||||
return
|
||||
|
||||
if prefix_len > 0:
|
||||
@@ -1005,7 +1007,7 @@ class PrefillAdder:
|
||||
else "shrunk"
|
||||
)
|
||||
self.tree_cache.finish_storage_prefetch_admission(
|
||||
req.rid,
|
||||
req.cache_request_handle,
|
||||
fulfilled_tokens=fulfilled_storage_hit,
|
||||
reason=reason,
|
||||
)
|
||||
|
||||
@@ -279,6 +279,7 @@ from sglang.srt.managers.utils import (
|
||||
validate_input_length,
|
||||
)
|
||||
from sglang.srt.mem_cache import kv_cache_builder
|
||||
from sglang.srt.mem_cache.base_prefix_cache import CacheRequestOutcome
|
||||
from sglang.srt.mem_cache.common import (
|
||||
maybe_cache_unfinished_req,
|
||||
release_kv_cache,
|
||||
@@ -3111,7 +3112,7 @@ class Scheduler(
|
||||
else None
|
||||
)
|
||||
tree_cache.prefetch_from_storage(
|
||||
req.rid,
|
||||
req.cache_request_handle,
|
||||
last_host_node,
|
||||
new_input_tokens,
|
||||
tree_cache.get_last_hash_value(last_host_node),
|
||||
@@ -3131,7 +3132,7 @@ class Scheduler(
|
||||
return
|
||||
max_attempts = get_memory().hicache_storage_prefetch_retry_max_attempts
|
||||
for req in self.waiting_queue:
|
||||
if self.tree_cache.pop_storage_prefetch_miss(req.rid):
|
||||
if self.tree_cache.pop_storage_prefetch_miss(req.cache_request_handle):
|
||||
req.storage_prefetch_retry_pending = True
|
||||
req.storage_prefetch_retry_wait_polls = 0
|
||||
if (
|
||||
@@ -3208,14 +3209,9 @@ class Scheduler(
|
||||
return False
|
||||
return True
|
||||
|
||||
def _release_aborted_request(self, rid: str) -> None:
|
||||
def _release_aborted_request(self, req: Req) -> None:
|
||||
"""Drop the cache-side state an aborted request left behind."""
|
||||
if (
|
||||
self.enable_hierarchical_cache
|
||||
or self.enable_hicache_storage
|
||||
or self.enable_unified_cache_external_linker
|
||||
):
|
||||
self.tree_cache.release_aborted_request(rid)
|
||||
self.tree_cache.finish(req.cache_request_handle, CacheRequestOutcome.ABORT)
|
||||
|
||||
def _abort_on_queued_limit(self, recv_req: Req) -> bool:
|
||||
"""Abort an incoming or existing request if the waiting queue is full. Returns True if the incoming request is aborted."""
|
||||
@@ -3243,7 +3239,7 @@ class Scheduler(
|
||||
direction * recv_req.priority < direction * candidate_req.priority
|
||||
)
|
||||
if abort_existing_req:
|
||||
self._release_aborted_request(candidate_req.rid)
|
||||
self._release_aborted_request(candidate_req)
|
||||
self.waiting_queue.pop(idx)
|
||||
self.beam_coordinator.retire_group(candidate_req)
|
||||
req_to_abort = candidate_req
|
||||
@@ -3457,7 +3453,7 @@ class Scheduler(
|
||||
req, self.req_to_metadata_buffer_idx_allocator
|
||||
)
|
||||
req.pending_bootstrap = False
|
||||
self._release_aborted_request(req.rid)
|
||||
self._release_aborted_request(req)
|
||||
release_kv_cache(req, self.tree_cache, is_insert=False)
|
||||
|
||||
self.chunked_req = None
|
||||
@@ -3855,14 +3851,16 @@ class Scheduler(
|
||||
break
|
||||
|
||||
if self.enable_hicache_storage:
|
||||
prefetch_done = self.tree_cache.check_prefetch_progress(req.rid)
|
||||
prefetch_done = self.tree_cache.check_prefetch_progress(
|
||||
req.cache_request_handle
|
||||
)
|
||||
if not prefetch_done:
|
||||
# skip staging requests that are ongoing prefetch
|
||||
continue
|
||||
# Pop the L3-loaded span. Unified cache exposes its absolute
|
||||
# start so cache-mode L2/L3 attribution survives L3-tail eviction.
|
||||
loaded_tokens, loaded_start = self.tree_cache.pop_prefetch_loaded_span(
|
||||
req.rid
|
||||
req.cache_request_handle
|
||||
)
|
||||
if loaded_tokens > 0:
|
||||
req.storage_hit_length = loaded_tokens
|
||||
@@ -3886,7 +3884,7 @@ class Scheduler(
|
||||
# (fenced in init_hicache) will need the same charge via
|
||||
# mamba_host_hit_length.
|
||||
held_tokens, held_swa_tokens = self.tree_cache.plan_staged_splice(
|
||||
req.rid, len(req.prefix_indices)
|
||||
req.cache_request_handle, len(req.prefix_indices)
|
||||
)
|
||||
if held_tokens > 0:
|
||||
req.host_hit_length = held_tokens
|
||||
@@ -5203,7 +5201,7 @@ class Scheduler(
|
||||
# This only works for requests that have not started anything.
|
||||
# We still need to send something back to TokenizerManager to clean up the state.
|
||||
req = self.waiting_queue.pop(i)
|
||||
self._release_aborted_request(req.rid)
|
||||
self._release_aborted_request(req)
|
||||
self.beam_coordinator.retire_group(req)
|
||||
# Without the initiator's reason the tokenizer falls back to a
|
||||
# generic abort message.
|
||||
@@ -5239,7 +5237,7 @@ class Scheduler(
|
||||
for req in self.dllm_manager.pop_aborted_reqs(
|
||||
recv_req.abort_all, recv_req.rid
|
||||
):
|
||||
self._release_aborted_request(req.rid)
|
||||
self._release_aborted_request(req)
|
||||
self.ipc_channels.send_to_tokenizer.send_output(
|
||||
_make_abort_req(req), req
|
||||
)
|
||||
@@ -5259,7 +5257,7 @@ class Scheduler(
|
||||
for req in self.disagg_prefill_bootstrap_queue.queue:
|
||||
if recv_req.abort_all or req.rid.startswith(recv_req.rid):
|
||||
logger.debug(f"Abort bootstrap queue request. {req.rid=}")
|
||||
self._release_aborted_request(req.rid)
|
||||
self._release_aborted_request(req)
|
||||
|
||||
if hasattr(req.disagg_kv_sender, "abort"):
|
||||
req.disagg_kv_sender.abort()
|
||||
|
||||
@@ -3,6 +3,7 @@ from __future__ import annotations
|
||||
import dataclasses
|
||||
import time
|
||||
from abc import ABC, abstractmethod
|
||||
from enum import Enum, auto
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
@@ -38,6 +39,17 @@ if TYPE_CHECKING:
|
||||
)
|
||||
|
||||
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class CacheRequestHandle:
|
||||
rid: str
|
||||
attempt_id: int
|
||||
|
||||
|
||||
class CacheRequestOutcome(Enum):
|
||||
SUCCESS = auto()
|
||||
ABORT = auto()
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class PrefixCacheTrait(Protocol):
|
||||
req_to_token_pool: ReqToTokenPool
|
||||
@@ -346,6 +358,14 @@ class BasePrefixCache(ABC, PrefixCacheTrait):
|
||||
tens of seconds (see HostKVCache.destroy). Idempotent.
|
||||
"""
|
||||
|
||||
def release_aborted_request(self, handle: CacheRequestHandle) -> None:
|
||||
"""Release attempt state; caches without prefetch state have nothing to drop."""
|
||||
|
||||
def finish(self, handle: CacheRequestHandle, outcome: CacheRequestOutcome) -> None:
|
||||
"""Finish an attempt without cancelling successful asynchronous cache work."""
|
||||
if outcome != CacheRequestOutcome.SUCCESS:
|
||||
self.release_aborted_request(handle)
|
||||
|
||||
@abstractmethod
|
||||
def reset(self):
|
||||
pass
|
||||
@@ -484,19 +504,24 @@ class BasePrefixCache(ABC, PrefixCacheTrait):
|
||||
raise NotImplementedError()
|
||||
|
||||
def finish_storage_prefetch_admission(
|
||||
self, req_id: str, fulfilled_tokens: int, reason: Optional[str]
|
||||
self,
|
||||
handle: CacheRequestHandle,
|
||||
fulfilled_tokens: int,
|
||||
reason: Optional[str],
|
||||
) -> None:
|
||||
"""Resolve storage-hit accounting once a request is admitted.
|
||||
|
||||
Non-storage caches have no lifecycle state to resolve.
|
||||
"""
|
||||
|
||||
def discard_storage_prefetch_accounting(self, req_id: str) -> None:
|
||||
def discard_storage_prefetch_accounting(self, handle: CacheRequestHandle) -> None:
|
||||
"""Forget storage-hit lifecycle state without emitting a result."""
|
||||
|
||||
def pop_prefetch_loaded_span(self, req_id: str) -> tuple[int, Optional[int]]:
|
||||
def pop_prefetch_loaded_span(
|
||||
self, handle: CacheRequestHandle
|
||||
) -> tuple[int, Optional[int]]:
|
||||
"""Pop L3-loaded tokens and their absolute prefix start, if known."""
|
||||
return self.pop_prefetch_loaded_tokens(req_id), None
|
||||
return self.pop_prefetch_loaded_tokens(handle), None
|
||||
|
||||
def ready_to_load_host_cache(self) -> Any:
|
||||
"""
|
||||
|
||||
@@ -36,6 +36,7 @@ import torch
|
||||
from sglang.srt.environ import envs
|
||||
from sglang.srt.managers.cache_controller import HICACHE_WRITE_STAGING_POOL_FRACTION
|
||||
from sglang.srt.mem_cache.base_prefix_cache import (
|
||||
CacheRequestHandle,
|
||||
DecLockRefParams,
|
||||
EvictParams,
|
||||
InitLoadBackParams,
|
||||
@@ -99,7 +100,7 @@ class _StagedPrefetch(msgspec.Struct):
|
||||
the op-owned host bounce exists (no device state, nothing in the tree).
|
||||
"""
|
||||
|
||||
req_id: str
|
||||
request: CacheRequestHandle
|
||||
key_tokens: list[int]
|
||||
extra_key: Optional[str]
|
||||
cache_salt: Optional[str]
|
||||
@@ -117,7 +118,7 @@ class _OngoingBufferLoadBack(msgspec.Struct):
|
||||
tree-resident; only the host bounce remains to free.
|
||||
"""
|
||||
|
||||
req_id: str
|
||||
request: CacheRequestHandle
|
||||
num_tokens: int
|
||||
occupied_tokens: int
|
||||
aux_xfers: list[PoolTransfer]
|
||||
@@ -272,9 +273,9 @@ class BufferModePipeline:
|
||||
# negative ack id).
|
||||
self.pending_hit_allocs: deque = deque()
|
||||
self._prefetch_prefix_ctx: dict[
|
||||
str, tuple[list[int], Optional[str], Optional[str]]
|
||||
CacheRequestHandle, tuple[list[int], Optional[str], Optional[str]]
|
||||
] = {}
|
||||
self.staged_prefetches: dict[str, _StagedPrefetch] = {}
|
||||
self.staged_prefetches: dict[CacheRequestHandle, _StagedPrefetch] = {}
|
||||
self.ongoing_buffer_load_back: dict[int, _OngoingBufferLoadBack] = {}
|
||||
# Backup pipeline: FIFO intents awaiting a D2H slot, node ids
|
||||
# anywhere in flight (dedupes re-triggers), and a content refcount
|
||||
@@ -292,8 +293,8 @@ class BufferModePipeline:
|
||||
self.write_staged_tokens_ = 0
|
||||
self.write_backlog_tokens_ = 0
|
||||
self._backlog_cap_hits = 0
|
||||
# rid-keyed anchor locks; released idempotently at every exit.
|
||||
self.anchor_locks: dict[str, _AnchorLock] = {}
|
||||
# Attempt-keyed locks stop stale completions from unlocking retries.
|
||||
self.anchor_locks: dict[CacheRequestHandle, _AnchorLock] = {}
|
||||
self.anchor_locked_tokens_ = 0
|
||||
self._anchor_lock_cap_skips = 0
|
||||
|
||||
@@ -743,16 +744,16 @@ class BufferModePipeline:
|
||||
|
||||
# ---- load back pipeline (storage -> staging -> device) ----
|
||||
|
||||
def try_lock_anchor(self, req_id: str) -> str:
|
||||
def try_lock_anchor(self, request: CacheRequestHandle) -> str:
|
||||
"""Pin the staged prefetch's device anchor so eviction cannot
|
||||
invalidate the splice, finding it by re-matching the live tree
|
||||
(carried node ids go stale via splits and eviction; the walk is
|
||||
O(prefix path)). Returns "locked", "no_anchor" (nothing to pin),
|
||||
"cap_skip" (over cap; launches unlocked), or "anchor_lost" (splice
|
||||
base gone — the caller cancels the storage IO)."""
|
||||
if req_id in self.anchor_locks:
|
||||
if request in self.anchor_locks:
|
||||
return "locked"
|
||||
prefix_ctx = self._prefetch_prefix_ctx.get(req_id)
|
||||
prefix_ctx = self._prefetch_prefix_ctx.get(request)
|
||||
if not prefix_ctx or not prefix_ctx[0]:
|
||||
return "no_anchor" # root anchor: nothing to pin
|
||||
prefix_tokens, extra_key, cache_salt = prefix_ctx
|
||||
@@ -777,7 +778,7 @@ class BufferModePipeline:
|
||||
if cache.tree_core.is_eagle:
|
||||
# The suffix owns the boundary token shared with the last matched
|
||||
# bigram, so include it when rebuilding the anchor key.
|
||||
info = cache.ongoing_prefetch.get(req_id)
|
||||
info = cache.ongoing_prefetch.get(request)
|
||||
if info is None or not info.prefetch_key.token_ids:
|
||||
return "anchor_lost"
|
||||
anchor_tokens.append(info.prefetch_key.token_ids[0])
|
||||
@@ -794,7 +795,7 @@ class BufferModePipeline:
|
||||
if len(match.device_indices) < matched_len:
|
||||
return "anchor_lost"
|
||||
lock_params = cache.inc_lock_ref(match.last_device_node).to_dec_params()
|
||||
self.anchor_locks[req_id] = _AnchorLock(
|
||||
self.anchor_locks[request] = _AnchorLock(
|
||||
node_id=match.last_device_node,
|
||||
lock_params=lock_params,
|
||||
tokens=matched_len,
|
||||
@@ -802,28 +803,30 @@ class BufferModePipeline:
|
||||
self.anchor_locked_tokens_ += matched_len
|
||||
return "locked"
|
||||
|
||||
def release_anchor_lock(self, req_id: str) -> None:
|
||||
def release_anchor_lock(self, request: CacheRequestHandle) -> None:
|
||||
"""Drop a staged prefetch's anchor lock (idempotent; called at every
|
||||
consume/drop/abort exit)."""
|
||||
lock = self.anchor_locks.pop(req_id, None)
|
||||
lock = self.anchor_locks.pop(request, None)
|
||||
if lock is None:
|
||||
return
|
||||
self._cache.dec_lock_ref(lock.node_id, lock.lock_params)
|
||||
self.anchor_locked_tokens_ -= lock.tokens
|
||||
assert self.anchor_locked_tokens_ >= 0, (
|
||||
f"anchor-lock accounting corrupted: locked={self.anchor_locked_tokens_} "
|
||||
f"after releasing {req_id}"
|
||||
f"after releasing {request.rid}"
|
||||
)
|
||||
|
||||
def staged_span_covered(self, req_id: str, span_tokens: int) -> bool:
|
||||
def staged_span_covered(
|
||||
self, request: CacheRequestHandle, span_tokens: int
|
||||
) -> bool:
|
||||
"""True when the live device tree already covers the fetch's whole
|
||||
would-be span (prefix + the storage-hit tokens): nothing would be
|
||||
left to splice at consumption, so the IO-commit caller cancels
|
||||
before the bounce alloc and the storage read."""
|
||||
info = self._cache.ongoing_prefetch.get(req_id)
|
||||
info = self._cache.ongoing_prefetch.get(request)
|
||||
if info is None or span_tokens <= 0:
|
||||
return False
|
||||
prefix_tokens, _, _ = self._prefetch_prefix_ctx[req_id]
|
||||
prefix_tokens, _, _ = self._prefetch_prefix_ctx[request]
|
||||
span_key = info.prefetch_key
|
||||
full_tokens = array("q", prefix_tokens)
|
||||
full_tokens.extend(span_key[:span_tokens].token_ids)
|
||||
@@ -838,7 +841,7 @@ class BufferModePipeline:
|
||||
|
||||
def set_prefix_ctx(
|
||||
self,
|
||||
req_id: str,
|
||||
request: CacheRequestHandle,
|
||||
matched_prefix_tokens,
|
||||
extra_key: Optional[str] = None,
|
||||
cache_salt: Optional[str] = None,
|
||||
@@ -846,17 +849,17 @@ class BufferModePipeline:
|
||||
"""Record the device-matched prefix (and its tree-key namespace) at
|
||||
prefetch enqueue; consumed at staging commit to build the full-span
|
||||
tree key, and by try_lock_anchor to re-match a stale anchor."""
|
||||
self._prefetch_prefix_ctx[req_id] = (
|
||||
self._prefetch_prefix_ctx[request] = (
|
||||
list(matched_prefix_tokens or []),
|
||||
extra_key,
|
||||
cache_salt,
|
||||
)
|
||||
|
||||
def pop_prefix_ctx(self, req_id: str) -> None:
|
||||
self._prefetch_prefix_ctx.pop(req_id, None)
|
||||
def pop_prefix_ctx(self, request: CacheRequestHandle) -> None:
|
||||
self._prefetch_prefix_ctx.pop(request, None)
|
||||
|
||||
def has_staged(self, req_id: str) -> bool:
|
||||
return req_id in self.staged_prefetches
|
||||
def has_staged(self, request: CacheRequestHandle) -> bool:
|
||||
return request in self.staged_prefetches
|
||||
|
||||
@staticmethod
|
||||
def _occupied_span(host_indices) -> int:
|
||||
@@ -866,7 +869,7 @@ class BufferModePipeline:
|
||||
|
||||
def stage_completed_prefetch(
|
||||
self,
|
||||
req_id: str,
|
||||
request: CacheRequestHandle,
|
||||
num_tokens: int,
|
||||
hash_value: list[str],
|
||||
) -> bool:
|
||||
@@ -881,9 +884,9 @@ class BufferModePipeline:
|
||||
operation,
|
||||
_lock_params,
|
||||
comp_xfers,
|
||||
) = cache.ongoing_prefetch.pop(req_id)
|
||||
) = cache.ongoing_prefetch.pop(request)
|
||||
cc = cache.cache_controller
|
||||
prefix_ctx = self._prefetch_prefix_ctx.pop(req_id, None)
|
||||
prefix_ctx = self._prefetch_prefix_ctx.pop(request, None)
|
||||
prefix_tokens = prefix_ctx[0] if prefix_ctx is not None else None
|
||||
aux_xfers = [x for xfers in comp_xfers.values() for x in xfers]
|
||||
# Component transfers are already present in comp_xfers. Preserve the
|
||||
@@ -897,14 +900,14 @@ class BufferModePipeline:
|
||||
|
||||
if num_tokens == 0 or prefix_tokens is None:
|
||||
# Nothing usable fetched: recompute.
|
||||
cache.discard_storage_prefetch_accounting(req_id)
|
||||
self.release_anchor_lock(req_id)
|
||||
cache.discard_storage_prefetch_accounting(request)
|
||||
self.release_anchor_lock(request)
|
||||
cc.append_host_mem_release(
|
||||
host_indices[:num_tokens], extra_pools=aux_xfers or None
|
||||
)
|
||||
cc.prefetch_tokens_occupied -= self._occupied_span(host_indices)
|
||||
cache.prefetch_loaded_tokens_by_reqid[req_id] = 0
|
||||
cache.prefetch_loaded_storage_start_by_reqid.pop(req_id, None)
|
||||
cache.prefetch_loaded_tokens_by_reqid[request] = 0
|
||||
cache.prefetch_loaded_storage_start_by_reqid.pop(request, None)
|
||||
return True
|
||||
|
||||
staged_pages = num_tokens // cache.page_size
|
||||
@@ -916,8 +919,8 @@ class BufferModePipeline:
|
||||
cache.storage_existence_cache.add(PoolName.KV, list(staged_hashes))
|
||||
occupied_tokens = self._occupied_span(host_indices)
|
||||
|
||||
self.staged_prefetches[req_id] = _StagedPrefetch(
|
||||
req_id=req_id,
|
||||
self.staged_prefetches[request] = _StagedPrefetch(
|
||||
request=request,
|
||||
key_tokens=prefix_tokens + list(prefetch_key[:num_tokens].token_ids),
|
||||
extra_key=prefetch_key.extra_key,
|
||||
cache_salt=prefetch_key.cache_salt,
|
||||
@@ -929,18 +932,18 @@ class BufferModePipeline:
|
||||
hash_values=staged_hashes,
|
||||
operation_id=operation.id,
|
||||
)
|
||||
cache.prefetch_loaded_tokens_by_reqid[req_id] = num_tokens
|
||||
cache.prefetch_loaded_storage_start_by_reqid[req_id] = operation.storage_start
|
||||
cache.prefetch_loaded_tokens_by_reqid[request] = num_tokens
|
||||
cache.prefetch_loaded_storage_start_by_reqid[request] = operation.storage_start
|
||||
return True
|
||||
|
||||
def plan_staged_splice(
|
||||
self, req_id: str, device_prefix_len: int
|
||||
self, request: CacheRequestHandle, device_prefix_len: int
|
||||
) -> tuple[int, int]:
|
||||
"""(kv, swa) host-hit tokens consumption will splice given the
|
||||
request's live device prefix, so admission charges no phantom
|
||||
tokens. Frees a hold that can no longer splice: surfaced as 0 but
|
||||
kept, it would leak — the adder only consumes surfaced host hits."""
|
||||
f = self.staged_prefetches.get(req_id)
|
||||
f = self.staged_prefetches.get(request)
|
||||
if f is None:
|
||||
return 0, 0
|
||||
splice_tokens = staged_splice_tokens(f, device_prefix_len)
|
||||
@@ -949,28 +952,28 @@ class BufferModePipeline:
|
||||
logger.info(
|
||||
"HiCache staged prefetch released req=%s matched=%d "
|
||||
"device_prefix=%d tokens=%d",
|
||||
req_id,
|
||||
request.rid,
|
||||
f.matched_len,
|
||||
device_prefix_len,
|
||||
f.num_tokens,
|
||||
)
|
||||
reason = None if covered_tokens == f.num_tokens else "shrunk"
|
||||
self.release_staged_hold(req_id, reason=reason)
|
||||
self.release_staged_hold(request, reason=reason)
|
||||
return 0, 0
|
||||
return splice_tokens, self.staged_prefetch_swa_tokens(req_id)
|
||||
return splice_tokens, self.staged_prefetch_swa_tokens(request)
|
||||
|
||||
def _resolve_staged_device_coverage(
|
||||
self, f: _StagedPrefetch, device_prefix_len: int
|
||||
) -> int:
|
||||
covered_tokens = min(max(device_prefix_len - f.matched_len, 0), f.num_tokens)
|
||||
self._cache._resolve_storage_prefetch_tokens(f.req_id, covered_tokens)
|
||||
self._cache._resolve_storage_prefetch_tokens(f.request, covered_tokens)
|
||||
return covered_tokens
|
||||
|
||||
def staged_prefetch_swa_tokens(self, req_id: str) -> int:
|
||||
def staged_prefetch_swa_tokens(self, request: CacheRequestHandle) -> int:
|
||||
"""SWA device tokens consuming this staged prefetch will allocate (the
|
||||
staged trailing window); surfaced as the request's swa_host_hit_length
|
||||
so the adder's SWA gate charges the admission-time alloc."""
|
||||
f = self.staged_prefetches.get(req_id)
|
||||
f = self.staged_prefetches.get(request)
|
||||
if f is None:
|
||||
return 0
|
||||
return sum(
|
||||
@@ -993,17 +996,18 @@ class BufferModePipeline:
|
||||
cache = self._cache
|
||||
req = params.req
|
||||
assert req is not None
|
||||
request = req.cache_request_handle
|
||||
empty = cache.tree_core.empty_match_result.device_indices
|
||||
unchanged = (empty, req.last_node)
|
||||
f = self.staged_prefetches.pop(req.rid, None)
|
||||
f = self.staged_prefetches.pop(request, None)
|
||||
if f is None:
|
||||
self.release_anchor_lock(req.rid)
|
||||
self.release_anchor_lock(request)
|
||||
return unchanged
|
||||
cc = cache.cache_controller
|
||||
|
||||
def _drop(reason: Optional[str]) -> tuple[torch.Tensor, NodeId]:
|
||||
cache._finish_storage_prefetch(req.rid, fulfilled_tokens=0, reason=reason)
|
||||
self.release_anchor_lock(req.rid)
|
||||
cache._finish_storage_prefetch(request, fulfilled_tokens=0, reason=reason)
|
||||
self.release_anchor_lock(request)
|
||||
self._free_staging_now(f.host_indices, f.aux_xfers)
|
||||
cc.prefetch_tokens_occupied -= f.occupied_tokens
|
||||
# Nothing spliced: keep the surfaced host-hit fields truthful.
|
||||
@@ -1038,7 +1042,7 @@ class BufferModePipeline:
|
||||
f.matched_len,
|
||||
splice_base,
|
||||
f.num_tokens,
|
||||
req.rid in self.anchor_locks,
|
||||
request in self.anchor_locks,
|
||||
)
|
||||
reason = None if covered_tokens == f.num_tokens else "shrunk"
|
||||
return _drop(reason)
|
||||
@@ -1047,7 +1051,7 @@ class BufferModePipeline:
|
||||
f"staged splice trim not page-aligned req={req.rid}: "
|
||||
f"matched={f.matched_len} splice_base={splice_base}"
|
||||
)
|
||||
cache._resolve_storage_prefetch_tokens(req.rid, trim_tokens)
|
||||
cache._resolve_storage_prefetch_tokens(request, trim_tokens)
|
||||
|
||||
key = RadixKey(
|
||||
array("q", f.key_tokens),
|
||||
@@ -1075,7 +1079,7 @@ class BufferModePipeline:
|
||||
len(live.device_indices),
|
||||
live.full_kv_hit_length,
|
||||
f.num_tokens,
|
||||
req.rid in self.anchor_locks,
|
||||
request in self.anchor_locks,
|
||||
)
|
||||
available_end = min(
|
||||
span_end,
|
||||
@@ -1083,7 +1087,7 @@ class BufferModePipeline:
|
||||
live.full_kv_hit_length,
|
||||
)
|
||||
available_overlap = max(0, available_end - splice_base)
|
||||
cache._resolve_storage_prefetch_tokens(req.rid, available_overlap)
|
||||
cache._resolve_storage_prefetch_tokens(request, available_overlap)
|
||||
return _drop(None if available_overlap == splice_tokens else "shrunk")
|
||||
|
||||
# Evict-before-alloc (mirrors _load_back_transfers): the budget gate
|
||||
@@ -1145,7 +1149,7 @@ class BufferModePipeline:
|
||||
)
|
||||
)
|
||||
self.ongoing_buffer_load_back[load_back_id] = _OngoingBufferLoadBack(
|
||||
req_id=f.req_id,
|
||||
request=f.request,
|
||||
num_tokens=splice_tokens,
|
||||
occupied_tokens=f.occupied_tokens,
|
||||
aux_xfers=f.aux_xfers,
|
||||
@@ -1155,7 +1159,7 @@ class BufferModePipeline:
|
||||
hash_values=f.hash_values,
|
||||
)
|
||||
m = cache.match_prefix(MatchPrefixParams(key=key))
|
||||
self.release_anchor_lock(req.rid)
|
||||
self.release_anchor_lock(request)
|
||||
canonical = m.device_indices[splice_base:span_end]
|
||||
if len(m.device_indices) < span_end or not torch.equal(
|
||||
canonical, device_indices
|
||||
@@ -1163,7 +1167,7 @@ class BufferModePipeline:
|
||||
# Fail-stop: the insert freed or replaced slots the in-flight H2D
|
||||
# still targets; continuing risks silent KV corruption.
|
||||
raise RuntimeError(
|
||||
f"HiCache buffer load-back ownership violation req={f.req_id}: "
|
||||
f"HiCache buffer load-back ownership violation req={f.request.rid}: "
|
||||
f"insert prefix_len={insert_result.prefix_len} "
|
||||
f"expected={splice_base}, adopted={len(m.device_indices)} "
|
||||
f"span_end={span_end}, canonical_matches_incoming="
|
||||
@@ -1191,26 +1195,28 @@ class BufferModePipeline:
|
||||
cc.prefetch_tokens_occupied -= f.occupied_tokens
|
||||
logger.info(
|
||||
"HiCache prefetch fill committed req=%s filled=%d occupied=%d locked=%d",
|
||||
f.req_id,
|
||||
f.request.rid,
|
||||
f.num_tokens,
|
||||
cc.prefetch_tokens_occupied,
|
||||
self.anchor_locked_tokens_,
|
||||
)
|
||||
cache._finish_storage_prefetch(
|
||||
f.req_id, fulfilled_tokens=f.num_tokens, reason=None
|
||||
f.request, fulfilled_tokens=f.num_tokens, reason=None
|
||||
)
|
||||
return True
|
||||
|
||||
def release_staged_hold(self, rid: str, reason: Optional[str] = None) -> bool:
|
||||
def release_staged_hold(
|
||||
self, request: CacheRequestHandle, reason: Optional[str] = None
|
||||
) -> bool:
|
||||
"""Free a staged hold outright — anchor pin, host bounce (KV + aux),
|
||||
occupancy grant; nothing device-side exists yet. Called for aborts
|
||||
and for holds that can no longer splice. Returns True when a hold
|
||||
existed."""
|
||||
self.release_anchor_lock(rid)
|
||||
staged = self.staged_prefetches.pop(rid, None)
|
||||
self.release_anchor_lock(request)
|
||||
staged = self.staged_prefetches.pop(request, None)
|
||||
if staged is None:
|
||||
return False
|
||||
self._cache._finish_storage_prefetch(rid, fulfilled_tokens=0, reason=reason)
|
||||
self._cache._finish_storage_prefetch(request, fulfilled_tokens=0, reason=reason)
|
||||
self._free_staging_now(staged.host_indices, staged.aux_xfers)
|
||||
self._cache.cache_controller.prefetch_tokens_occupied -= staged.occupied_tokens
|
||||
return True
|
||||
|
||||
@@ -16,6 +16,7 @@ from sglang.srt.disaggregation.kv_events import StorageMedium
|
||||
from sglang.srt.distributed.communication_tags import P2PTag
|
||||
from sglang.srt.managers.cache_controller import HiCacheController, PrefetchOperation
|
||||
from sglang.srt.mem_cache.base_prefix_cache import (
|
||||
CacheRequestHandle,
|
||||
DecLockRefParams,
|
||||
DecLockRefResult,
|
||||
EvictParams,
|
||||
@@ -1511,8 +1512,13 @@ class HiRadixCache(RadixCache):
|
||||
extra_kwargs = {}
|
||||
if prefetch_op_cls is HybridPrefetchOperation:
|
||||
extra_kwargs["pool_transfers"] = self._get_extra_pools().get("extra_pools")
|
||||
request = (
|
||||
CacheRequestHandle("__storage_hit_query__", 0)
|
||||
if prefetch_op_cls is HybridPrefetchOperation
|
||||
else "__storage_hit_query__"
|
||||
)
|
||||
operation = prefetch_op_cls(
|
||||
"__storage_hit_query__",
|
||||
request,
|
||||
prefetch_key,
|
||||
last_hash,
|
||||
prefix_keys,
|
||||
@@ -1626,7 +1632,11 @@ class HiRadixCache(RadixCache):
|
||||
0, cc.prefetch_tokens_occupied - len(prefetch_key)
|
||||
)
|
||||
|
||||
def check_prefetch_progress(self, req_id: str) -> bool:
|
||||
def has_ongoing_prefetch(self, handle: CacheRequestHandle) -> bool:
|
||||
return handle.rid in self.ongoing_prefetch
|
||||
|
||||
def check_prefetch_progress(self, handle: CacheRequestHandle) -> bool:
|
||||
req_id = handle.rid
|
||||
if req_id not in self.ongoing_prefetch:
|
||||
# there is no ongoing prefetch for this request or it has been revoked
|
||||
return True
|
||||
@@ -1720,15 +1730,15 @@ class HiRadixCache(RadixCache):
|
||||
usable_pages = min(usable_pages, *pool_hit_pages)
|
||||
return usable_pages * self.page_size
|
||||
|
||||
def pop_prefetch_loaded_tokens(self, req_id: str) -> int:
|
||||
def pop_prefetch_loaded_tokens(self, handle: CacheRequestHandle) -> int:
|
||||
"""
|
||||
Pop and return the number of tokens loaded from storage for a request.
|
||||
Returns 0 if no prefetch was done or was revoked.
|
||||
This should be called after check_prefetch_progress() returns True.
|
||||
"""
|
||||
return self.prefetch_loaded_tokens_by_reqid.pop(req_id, 0)
|
||||
return self.prefetch_loaded_tokens_by_reqid.pop(handle.rid, 0)
|
||||
|
||||
def pop_storage_prefetch_miss(self, req_id: str) -> bool:
|
||||
def pop_storage_prefetch_miss(self, handle: CacheRequestHandle) -> bool:
|
||||
"""Storage prefetch miss markers are not tracked on the dense path;
|
||||
the scheduler's paced availability-check retry is inert here."""
|
||||
return False
|
||||
@@ -1768,7 +1778,7 @@ class HiRadixCache(RadixCache):
|
||||
|
||||
def prefetch_from_storage(
|
||||
self,
|
||||
req_id: str,
|
||||
handle: CacheRequestHandle,
|
||||
last_host_node: TreeNode,
|
||||
new_input_tokens: List[int],
|
||||
last_hash: Optional[str] = None,
|
||||
@@ -1778,6 +1788,7 @@ class HiRadixCache(RadixCache):
|
||||
extra_key: Optional[str] = None,
|
||||
cache_salt: Optional[str] = None,
|
||||
):
|
||||
req_id = handle.rid
|
||||
prefetch_key = RadixKey(
|
||||
new_input_tokens,
|
||||
extra_key=extra_key,
|
||||
@@ -1798,8 +1809,13 @@ class HiRadixCache(RadixCache):
|
||||
# NOTE: host_indices is no longer pre-allocated here. It is allocated
|
||||
# lazily in _drain_and_alloc_storage_hit() once the L3 storage hit count is known,
|
||||
# so we only reserve host memory for pages that actually hit.
|
||||
request = (
|
||||
handle
|
||||
if isinstance(self.cache_controller, HybridCacheController)
|
||||
else req_id
|
||||
)
|
||||
operation = self.cache_controller.prefetch(
|
||||
req_id,
|
||||
request,
|
||||
prefetch_key,
|
||||
last_hash,
|
||||
prefix_keys,
|
||||
@@ -1999,7 +2015,8 @@ class HiRadixCache(RadixCache):
|
||||
self._inc_hit_count(new_node, chunked)
|
||||
return InsertResult(prefix_len=total_prefix_length)
|
||||
|
||||
def release_aborted_request(self, rid: str):
|
||||
def release_aborted_request(self, handle: CacheRequestHandle):
|
||||
rid = handle.rid
|
||||
# Clean up storage hit tracking for aborted request
|
||||
self.prefetch_loaded_tokens_by_reqid.pop(rid, None)
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ from sglang.srt.managers.cache_controller import (
|
||||
from sglang.srt.managers.cache_controller import (
|
||||
StorageOperation as BaseStorageOperation,
|
||||
)
|
||||
from sglang.srt.mem_cache.base_prefix_cache import CacheRequestHandle
|
||||
from sglang.srt.mem_cache.hicache_storage import (
|
||||
HiCacheStorageExtraInfo,
|
||||
PoolHitPolicy,
|
||||
@@ -62,13 +63,14 @@ class StorageOperation(BaseStorageOperation):
|
||||
class PrefetchOperation(StorageOperation):
|
||||
def __init__(
|
||||
self,
|
||||
request_id: str,
|
||||
handle: CacheRequestHandle,
|
||||
token_ids: List[int],
|
||||
last_hash: Optional[str] = None,
|
||||
prefix_keys: Optional[List[str]] = None,
|
||||
pool_transfers: Optional[list[PoolTransfer]] = None,
|
||||
):
|
||||
self.request_id = request_id
|
||||
self.handle = handle
|
||||
self.request_id = handle.rid
|
||||
self._lock = threading.Lock()
|
||||
self._terminated_flag = False
|
||||
self.storage_hit_count = 0
|
||||
@@ -543,14 +545,14 @@ class HybridCacheController(BaseHiCacheController):
|
||||
|
||||
def prefetch(
|
||||
self,
|
||||
request_id: str,
|
||||
handle: CacheRequestHandle,
|
||||
new_input_tokens: List[int],
|
||||
last_hash: Optional[str] = None,
|
||||
prefix_keys: Optional[List[str]] = None,
|
||||
extra_pools: Optional[list[PoolTransfer]] = None,
|
||||
) -> PrefetchOperation:
|
||||
operation = PrefetchOperation(
|
||||
request_id,
|
||||
handle,
|
||||
new_input_tokens,
|
||||
last_hash,
|
||||
prefix_keys=prefix_keys,
|
||||
|
||||
@@ -35,6 +35,7 @@ from typing import Any, Dict, List, Optional, Tuple
|
||||
import numpy as np
|
||||
import torch
|
||||
|
||||
from sglang.srt.mem_cache.base_prefix_cache import CacheRequestHandle
|
||||
from sglang.srt.mem_cache.storage.flexkv.flexkv_comm import (
|
||||
CMD_LAYERWISE,
|
||||
CMD_PUT_META,
|
||||
@@ -219,14 +220,20 @@ class FlexKVConnector:
|
||||
|
||||
# 10. Per-rank in-flight tracking.
|
||||
# Loads
|
||||
self._pending_lookups: Dict[str, int] = {} # rid -> fkv_task_id
|
||||
self._inflight_loads: Dict[int, int] = {} # producer_id -> rid hashlike
|
||||
self._pending_lookups: Dict[
|
||||
CacheRequestHandle, int
|
||||
] = {} # handle -> fkv_task_id
|
||||
self._inflight_loads: Dict[int, int] = {} # producer_id -> task id
|
||||
self._completed_layerwise: List[int] = []
|
||||
self._launched_load_tids: List[int] = [] # leader-only, for periodic drain
|
||||
# Stores
|
||||
self._inflight_stores: Dict[str, int] = {} # rid -> fkv_task_id
|
||||
self._inflight_stores: Dict[
|
||||
CacheRequestHandle, int
|
||||
] = {} # handle -> fkv_task_id
|
||||
# Prefetches
|
||||
self._ongoing_prefetches: Dict[str, int] = {} # rid -> fkv_task_id
|
||||
self._ongoing_prefetches: Dict[
|
||||
CacheRequestHandle, int
|
||||
] = {} # handle -> fkv_task_id
|
||||
self._prefetch_enabled = bool(
|
||||
self.cache_config.enable_ssd
|
||||
or self.cache_config.enable_remote
|
||||
@@ -248,7 +255,7 @@ class FlexKVConnector:
|
||||
self,
|
||||
token_ids: List[int],
|
||||
token_mask: torch.Tensor,
|
||||
rid: Optional[str] = None,
|
||||
handle: Optional[CacheRequestHandle] = None,
|
||||
) -> Tuple[int, int]:
|
||||
"""Page-aligned prefix lookup against FlexKV.
|
||||
|
||||
@@ -256,8 +263,8 @@ class FlexKVConnector:
|
||||
token_ids: full token id sequence we'd like to check.
|
||||
token_mask: 1-D bool tensor or array, True for "this token is
|
||||
*not* already on GPU and is a candidate for load-back".
|
||||
rid: if set and hit > 0, the held FlexKV task id is stashed
|
||||
under this key so a later ``retrieve_kv(rid, slots)`` call
|
||||
handle: if set and hit > 0, the held FlexKV task id is stashed
|
||||
under this key so a later ``retrieve_kv(handle, slots)`` call
|
||||
can resolve it. If not set, the held task is cancelled when
|
||||
hit > 0 and the caller didn't ask to track it.
|
||||
|
||||
@@ -304,29 +311,29 @@ class FlexKVConnector:
|
||||
hit_length = aligned
|
||||
|
||||
# Decide what to do with the held task. Three cases:
|
||||
# 1. hit_length > 0 and rid given → stash for retrieve_kv later.
|
||||
# 2. hit_length > 0 and rid is None → cancel; caller can't use it.
|
||||
# 1. hit_length > 0 and handle given → stash for retrieve_kv later.
|
||||
# 2. hit_length > 0 and handle is None → cancel; caller can't use it.
|
||||
# 3. hit_length == 0 → no work to do; FlexKV already marked the
|
||||
# empty graph COMPLETED inside get_match, cancel would warn.
|
||||
if hit_length > 0 and rid is not None and fkv_task_id >= 0:
|
||||
self._pending_lookups[rid] = fkv_task_id
|
||||
if hit_length > 0 and handle is not None and fkv_task_id >= 0:
|
||||
self._pending_lookups[handle] = fkv_task_id
|
||||
elif hit_length > 0 and fkv_task_id >= 0 and self._sync_ctx.is_sync_leader:
|
||||
assert self.kv_manager is not None
|
||||
self.kv_manager.cancel([fkv_task_id])
|
||||
|
||||
return fkv_task_id, hit_length
|
||||
|
||||
def release_pending(self, rid: str) -> None:
|
||||
"""Cancel the task held by an earlier ``lookup_kv(rid=...)`` that
|
||||
def release_pending(self, handle: CacheRequestHandle) -> None:
|
||||
"""Cancel the task held by an earlier ``lookup_kv(handle=...)`` that
|
||||
won't be followed by a ``retrieve_kv`` (e.g. allocation failed)."""
|
||||
fkv_task_id = self._pending_lookups.pop(rid, -1)
|
||||
fkv_task_id = self._pending_lookups.pop(handle, -1)
|
||||
if fkv_task_id >= 0 and self._sync_ctx.is_sync_leader:
|
||||
assert self.kv_manager is not None
|
||||
self.kv_manager.cancel([fkv_task_id])
|
||||
|
||||
def retrieve_kv(
|
||||
self,
|
||||
rid: str,
|
||||
handle: CacheRequestHandle,
|
||||
slot_mapping: torch.Tensor,
|
||||
) -> int:
|
||||
"""Synchronous load: ``launch`` + ``wait``.
|
||||
@@ -335,7 +342,7 @@ class FlexKVConnector:
|
||||
responsible for having allocated ``slot_mapping`` of length
|
||||
equal to ``hit_length`` from a prior ``lookup_kv``.
|
||||
"""
|
||||
fkv_task_id = self._pending_lookups.pop(rid, -1)
|
||||
fkv_task_id = self._pending_lookups.pop(handle, -1)
|
||||
if fkv_task_id < 0:
|
||||
return 0
|
||||
|
||||
@@ -371,7 +378,7 @@ class FlexKVConnector:
|
||||
|
||||
def start_load_kv_layerwise(
|
||||
self,
|
||||
rid: str,
|
||||
handle: CacheRequestHandle,
|
||||
slot_mapping: torch.Tensor,
|
||||
) -> Tuple[int, int]:
|
||||
"""Layerwise load. Fires ``launch(layerwise_transfer=True)`` and
|
||||
@@ -382,7 +389,7 @@ class FlexKVConnector:
|
||||
"start_load_kv_layerwise called but layerwise transfer is "
|
||||
"disabled. Set FLEXKV_ENABLE_LAYERWISE_TRANSFER=1."
|
||||
)
|
||||
fkv_task_id = self._pending_lookups.pop(rid, -1)
|
||||
fkv_task_id = self._pending_lookups.pop(handle, -1)
|
||||
if fkv_task_id < 0:
|
||||
return 0, -1
|
||||
|
||||
@@ -450,7 +457,7 @@ class FlexKVConnector:
|
||||
|
||||
def store_kv(
|
||||
self,
|
||||
rid: str,
|
||||
handle: CacheRequestHandle,
|
||||
token_ids: List[int],
|
||||
kv_indices: torch.Tensor,
|
||||
) -> int:
|
||||
@@ -507,7 +514,7 @@ class FlexKVConnector:
|
||||
as_batch=False,
|
||||
layerwise_transfer=False,
|
||||
)
|
||||
self._inflight_stores[rid] = fkv_task_id
|
||||
self._inflight_stores[handle] = fkv_task_id
|
||||
return fkv_task_id
|
||||
return -1
|
||||
|
||||
@@ -530,28 +537,28 @@ class FlexKVConnector:
|
||||
filtered = kv_indices[unmatched_mask]
|
||||
slot_mapping_cpu = self._to_cpu_int64(filtered)
|
||||
self._send_slot_mapping_to_remote(fkv_task_id, slot_mapping_cpu)
|
||||
self._inflight_stores[rid] = fkv_task_id
|
||||
self._inflight_stores[handle] = fkv_task_id
|
||||
return fkv_task_id
|
||||
|
||||
def check_completed_stores(self) -> List[str]:
|
||||
"""Return rids whose stores have completed since the last call."""
|
||||
completed_rids: List[str] = []
|
||||
def check_completed_stores(self) -> List[CacheRequestHandle]:
|
||||
"""Return request handles whose stores have completed since the last call."""
|
||||
completed_handles: List[CacheRequestHandle] = []
|
||||
completed_dict: Dict[int, Any] = {}
|
||||
|
||||
if self._sync_ctx.is_sync_leader and self.kv_manager is not None:
|
||||
if self._inflight_stores:
|
||||
fk_to_rid = {v: k for k, v in self._inflight_stores.items()}
|
||||
fk_to_handle = {v: k for k, v in self._inflight_stores.items()}
|
||||
try:
|
||||
completed_dict = self.kv_manager.try_wait(
|
||||
task_ids=list(fk_to_rid.keys())
|
||||
task_ids=list(fk_to_handle.keys())
|
||||
)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
logger.debug("[FlexKV] check_completed_stores: %s", exc)
|
||||
completed_dict = {}
|
||||
for fk_tid in completed_dict:
|
||||
rid = fk_to_rid[fk_tid]
|
||||
completed_rids.append(rid)
|
||||
self._inflight_stores.pop(rid, None)
|
||||
handle = fk_to_handle[fk_tid]
|
||||
completed_handles.append(handle)
|
||||
self._inflight_stores.pop(handle, None)
|
||||
|
||||
if self._sync_ctx.is_pp_sender:
|
||||
self._sync_ctx.scatter_pp(
|
||||
@@ -569,20 +576,20 @@ class FlexKVConnector:
|
||||
)
|
||||
fk_ids = payload.get("completed_fk_ids", [])
|
||||
if fk_ids and self._inflight_stores:
|
||||
fk_to_rid = {v: k for k, v in self._inflight_stores.items()}
|
||||
fk_to_handle = {v: k for k, v in self._inflight_stores.items()}
|
||||
for fk_tid in fk_ids:
|
||||
if fk_tid in fk_to_rid:
|
||||
rid = fk_to_rid[fk_tid]
|
||||
completed_rids.append(rid)
|
||||
self._inflight_stores.pop(rid, None)
|
||||
if fk_tid in fk_to_handle:
|
||||
handle = fk_to_handle[fk_tid]
|
||||
completed_handles.append(handle)
|
||||
self._inflight_stores.pop(handle, None)
|
||||
|
||||
if self._sync_ctx.needs_sync:
|
||||
completed_rids = self._sync_ctx.scatter(completed_rids)
|
||||
return completed_rids
|
||||
completed_handles = self._sync_ctx.scatter(completed_handles)
|
||||
return completed_handles
|
||||
|
||||
def wait_store(self, rid: str, timeout: float = 30.0) -> bool:
|
||||
"""Block until a single store task identified by ``rid`` finishes."""
|
||||
fkv_task_id = self._inflight_stores.pop(rid, -1)
|
||||
def wait_store(self, handle: CacheRequestHandle, timeout: float = 30.0) -> bool:
|
||||
"""Block until a single store task identified by ``handle`` finishes."""
|
||||
fkv_task_id = self._inflight_stores.pop(handle, -1)
|
||||
if fkv_task_id < 0:
|
||||
return True
|
||||
if not self._sync_ctx.is_sync_leader or self.kv_manager is None:
|
||||
@@ -600,8 +607,8 @@ class FlexKVConnector:
|
||||
# Public API — prefetch
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def prefetch_async(self, rid: str, token_ids: List[int]) -> int:
|
||||
if not self._prefetch_enabled or not rid:
|
||||
def prefetch_async(self, handle: CacheRequestHandle, token_ids: List[int]) -> int:
|
||||
if not self._prefetch_enabled:
|
||||
return -1
|
||||
task_id = -1
|
||||
if self._sync_ctx.is_sync_leader and self.kv_manager is not None:
|
||||
@@ -616,13 +623,13 @@ class FlexKVConnector:
|
||||
payload = self._sync_ctx.scatter({"task_id": task_id})
|
||||
task_id = payload["task_id"]
|
||||
if task_id >= 0:
|
||||
self._ongoing_prefetches[rid] = task_id
|
||||
self._ongoing_prefetches[handle] = task_id
|
||||
return task_id
|
||||
|
||||
def check_prefetch_progress(self, rid: str) -> bool:
|
||||
def check_prefetch_progress(self, handle: CacheRequestHandle) -> bool:
|
||||
if not self._prefetch_enabled:
|
||||
return True
|
||||
task_id = self._ongoing_prefetches.get(rid, -1)
|
||||
task_id = self._ongoing_prefetches.get(handle, -1)
|
||||
if task_id < 0:
|
||||
return True
|
||||
done = False
|
||||
@@ -637,14 +644,14 @@ class FlexKVConnector:
|
||||
payload = self._sync_ctx.scatter({"done": done})
|
||||
done = payload["done"]
|
||||
if done:
|
||||
self._ongoing_prefetches.pop(rid, None)
|
||||
self._ongoing_prefetches.pop(handle, None)
|
||||
return done
|
||||
|
||||
def cancel_prefetch(self, rid: str) -> None:
|
||||
self._pending_lookups.pop(rid, None)
|
||||
def cancel_prefetch(self, handle: CacheRequestHandle) -> None:
|
||||
self._pending_lookups.pop(handle, None)
|
||||
# FlexKV doesn't currently support prefetch cancellation, but
|
||||
# we still drop our tracking entry.
|
||||
self._ongoing_prefetches.pop(rid, None)
|
||||
self._ongoing_prefetches.pop(handle, None)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Layerwise transfer hooks
|
||||
|
||||
@@ -34,6 +34,7 @@ from typing import TYPE_CHECKING, Optional, Tuple
|
||||
import torch
|
||||
|
||||
from sglang.srt.mem_cache.base_prefix_cache import (
|
||||
CacheRequestHandle,
|
||||
EvictParams,
|
||||
EvictResult,
|
||||
InitLoadBackParams,
|
||||
@@ -123,11 +124,11 @@ class FlexKVRadixCache(RadixCache):
|
||||
|
||||
# Two-phase MP load: stash marker between ``match_prefix`` and
|
||||
# ``init_load_back``.
|
||||
self._load_markers: dict[str, _LoadBackMarker] = {}
|
||||
self._load_markers: dict[CacheRequestHandle, _LoadBackMarker] = {}
|
||||
# ``store_kv`` is async — we keep a lock on the source node
|
||||
# until FlexKV signals completion, draining in ``evict`` /
|
||||
# ``check_hicache_events``.
|
||||
self._inflight_store_nodes: dict[str, TreeNode] = {}
|
||||
self._inflight_store_nodes: dict[CacheRequestHandle, TreeNode] = {}
|
||||
self._node_lock = threading.Lock()
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
@@ -205,7 +206,7 @@ class FlexKVRadixCache(RadixCache):
|
||||
token_mask[device_len:] = True
|
||||
|
||||
fkv_task_id, hit = self.flexkv_connector.lookup_kv(
|
||||
token_ids=token_ids, token_mask=token_mask, rid=req.rid
|
||||
token_ids=token_ids, token_mask=token_mask, handle=req.cache_request_handle
|
||||
)
|
||||
if hit <= 0:
|
||||
return base_res
|
||||
@@ -215,7 +216,7 @@ class FlexKVRadixCache(RadixCache):
|
||||
token_ids_snap = token_ids[:]
|
||||
else:
|
||||
token_ids_snap = token_ids
|
||||
self._load_markers[req.rid] = _LoadBackMarker(
|
||||
self._load_markers[req.cache_request_handle] = _LoadBackMarker(
|
||||
key=RadixKey(
|
||||
token_ids_snap,
|
||||
key.extra_key,
|
||||
@@ -249,10 +250,10 @@ class FlexKVRadixCache(RadixCache):
|
||||
# Quick LOOKUP first to discover how many slots we'd need.
|
||||
token_mask = torch.zeros(len(token_ids), dtype=torch.bool)
|
||||
token_mask[device_len:] = True
|
||||
# No rid here — IP mode self-pops; pass a synthetic stable key.
|
||||
synthetic_rid = f"_ip_{id(key)}"
|
||||
# No handle here — IP mode self-pops; pass a synthetic stable key.
|
||||
synthetic_handle = CacheRequestHandle(f"_ip_{id(key)}", 0)
|
||||
_, hit = self.flexkv_connector.lookup_kv(
|
||||
token_ids=token_ids, token_mask=token_mask, rid=synthetic_rid
|
||||
token_ids=token_ids, token_mask=token_mask, handle=synthetic_handle
|
||||
)
|
||||
if hit <= 0:
|
||||
return base_res
|
||||
@@ -263,7 +264,7 @@ class FlexKVRadixCache(RadixCache):
|
||||
uncached_len=hit,
|
||||
last_node=last_node,
|
||||
load_fn=lambda slot_mapping: self.flexkv_connector.start_load_kv_layerwise(
|
||||
synthetic_rid, slot_mapping
|
||||
synthetic_handle, slot_mapping
|
||||
)[0],
|
||||
)
|
||||
if result is None:
|
||||
@@ -288,12 +289,12 @@ class FlexKVRadixCache(RadixCache):
|
||||
load; inserts the resulting TreeNode."""
|
||||
req = params.req
|
||||
last_node: TreeNode = params.best_match_node
|
||||
marker = self._load_markers.pop(req.rid, None)
|
||||
marker = self._load_markers.pop(req.cache_request_handle, None)
|
||||
if marker is None:
|
||||
# ``match_prefix`` decided there was no work to do, but the
|
||||
# scheduler still called us. Release any held task and
|
||||
# return an empty load.
|
||||
self.flexkv_connector.release_pending(req.rid)
|
||||
self.flexkv_connector.release_pending(req.cache_request_handle)
|
||||
return (
|
||||
torch.empty((0,), dtype=torch.int64, device=self.device),
|
||||
last_node,
|
||||
@@ -305,7 +306,7 @@ class FlexKVRadixCache(RadixCache):
|
||||
uncached_len=params.host_hit_length,
|
||||
last_node=last_node,
|
||||
load_fn=lambda slot_mapping: self.flexkv_connector.retrieve_kv(
|
||||
req.rid, slot_mapping
|
||||
req.cache_request_handle, slot_mapping
|
||||
),
|
||||
)
|
||||
if result is None:
|
||||
@@ -313,7 +314,7 @@ class FlexKVRadixCache(RadixCache):
|
||||
# already cancels/cleans up on failure paths; release_pending
|
||||
# is idempotent for the case where allocation failed before
|
||||
# we even popped the held task.
|
||||
self.flexkv_connector.release_pending(req.rid)
|
||||
self.flexkv_connector.release_pending(req.cache_request_handle)
|
||||
return (
|
||||
torch.empty((0,), dtype=torch.int64, device=self.device),
|
||||
last_node,
|
||||
@@ -392,7 +393,7 @@ class FlexKVRadixCache(RadixCache):
|
||||
req, is_insert=is_insert, kv_len_to_handle=kv_len_to_handle
|
||||
)
|
||||
if not is_insert:
|
||||
self._load_markers.pop(req.rid, None)
|
||||
self._load_markers.pop(req.cache_request_handle, None)
|
||||
return
|
||||
|
||||
# Compute the committed prefix mirroring LMCRadixCache's logic.
|
||||
@@ -431,7 +432,7 @@ class FlexKVRadixCache(RadixCache):
|
||||
try:
|
||||
with torch.cuda.stream(self.store_stream):
|
||||
fkv_task_id = self.flexkv_connector.store_kv(
|
||||
rid=req.rid,
|
||||
handle=req.cache_request_handle,
|
||||
token_ids=list(token_ids),
|
||||
kv_indices=kv_indices,
|
||||
)
|
||||
@@ -446,7 +447,7 @@ class FlexKVRadixCache(RadixCache):
|
||||
return
|
||||
|
||||
with self._node_lock:
|
||||
self._inflight_store_nodes[req.rid] = new_last_node
|
||||
self._inflight_store_nodes[req.cache_request_handle] = new_last_node
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# evict + completion draining
|
||||
@@ -473,12 +474,12 @@ class FlexKVRadixCache(RadixCache):
|
||||
self.flexkv_connector.drain_launched_loads()
|
||||
|
||||
def _drain_completed_stores(self) -> None:
|
||||
completed_rids = self.flexkv_connector.check_completed_stores()
|
||||
if not completed_rids:
|
||||
completed_handles = self.flexkv_connector.check_completed_stores()
|
||||
if not completed_handles:
|
||||
return
|
||||
with self._node_lock:
|
||||
for rid in completed_rids:
|
||||
node = self._inflight_store_nodes.pop(rid, None)
|
||||
for handle in completed_handles:
|
||||
node = self._inflight_store_nodes.pop(handle, None)
|
||||
if node is not None:
|
||||
self.dec_lock_ref(node)
|
||||
|
||||
@@ -486,33 +487,33 @@ class FlexKVRadixCache(RadixCache):
|
||||
# Optional pass-throughs used by the scheduler
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def release_aborted_request(self, rid: str) -> None:
|
||||
def release_aborted_request(self, handle: CacheRequestHandle) -> None:
|
||||
"""Clean up tracking for an aborted request without invoking FlexKV."""
|
||||
self._load_markers.pop(rid, None)
|
||||
self._load_markers.pop(handle, None)
|
||||
with self._node_lock:
|
||||
node = self._inflight_store_nodes.pop(rid, None)
|
||||
node = self._inflight_store_nodes.pop(handle, None)
|
||||
if node is not None:
|
||||
self.dec_lock_ref(node)
|
||||
self.flexkv_connector.release_pending(rid)
|
||||
self.flexkv_connector.cancel_prefetch(rid)
|
||||
self.flexkv_connector.release_pending(handle)
|
||||
self.flexkv_connector.cancel_prefetch(handle)
|
||||
|
||||
def prefetch_from_storage(
|
||||
self, rid: str, last_host_node: TreeNode, token_ids
|
||||
self, handle: CacheRequestHandle, last_host_node: TreeNode, token_ids
|
||||
) -> None:
|
||||
"""Kick off an opportunistic prefetch (SSD/Remote → CPU)."""
|
||||
try:
|
||||
self.flexkv_connector.prefetch_async(rid, list(token_ids))
|
||||
self.flexkv_connector.prefetch_async(handle, list(token_ids))
|
||||
except Exception as exc: # noqa: BLE001
|
||||
logger.debug("[FlexKV] prefetch_from_storage: %s", exc)
|
||||
|
||||
def check_prefetch_progress(self, rid: str) -> bool:
|
||||
return self.flexkv_connector.check_prefetch_progress(rid)
|
||||
def check_prefetch_progress(self, handle: CacheRequestHandle) -> bool:
|
||||
return self.flexkv_connector.check_prefetch_progress(handle)
|
||||
|
||||
def terminate_prefetch(self, rid: str) -> None:
|
||||
self.flexkv_connector.cancel_prefetch(rid)
|
||||
def terminate_prefetch(self, handle: CacheRequestHandle) -> None:
|
||||
self.flexkv_connector.cancel_prefetch(handle)
|
||||
|
||||
def pop_prefetch_loaded_tokens(self, rid: str) -> int:
|
||||
# FlexKV doesn't expose per-rid prefetched token counts yet.
|
||||
def pop_prefetch_loaded_tokens(self, handle: CacheRequestHandle) -> int:
|
||||
# FlexKV doesn't expose per-handle prefetched token counts yet.
|
||||
return 0
|
||||
|
||||
@property
|
||||
|
||||
@@ -364,23 +364,23 @@ class StorageAttachment:
|
||||
cache = self._cache
|
||||
controller = cache.cache_controller
|
||||
|
||||
for req_id in list(cache.ongoing_prefetch):
|
||||
info = cache.ongoing_prefetch[req_id]
|
||||
for handle in list(cache.ongoing_prefetch):
|
||||
info = cache.ongoing_prefetch[handle]
|
||||
try:
|
||||
cache.discard_storage_prefetch_accounting(req_id)
|
||||
cache.discard_storage_prefetch_accounting(handle)
|
||||
if info.host_indices is None:
|
||||
# Host pages were never allocated for this operation.
|
||||
cache.revoke_pending_prefetch(req_id)
|
||||
cache.revoke_pending_prefetch(handle)
|
||||
continue
|
||||
completed_tokens, _ = controller.terminate_prefetch(info.operation)
|
||||
del cache.ongoing_prefetch[req_id]
|
||||
del cache.ongoing_prefetch[handle]
|
||||
if info.anchor_lock_params is not None:
|
||||
cache.dec_host_lock_ref(
|
||||
info.anchor_node_id, info.anchor_lock_params
|
||||
)
|
||||
if cache.buffer_pipeline is not None:
|
||||
cache.buffer_pipeline.pop_prefix_ctx(req_id)
|
||||
cache.buffer_pipeline.release_anchor_lock(req_id)
|
||||
cache.buffer_pipeline.pop_prefix_ctx(handle)
|
||||
cache.buffer_pipeline.release_anchor_lock(handle)
|
||||
controller.append_host_mem_release(
|
||||
host_indices=info.host_indices[:completed_tokens],
|
||||
extra_pools=[
|
||||
@@ -395,8 +395,8 @@ class StorageAttachment:
|
||||
),
|
||||
)
|
||||
except Exception:
|
||||
logger.exception("Failed to release pending prefetch %s", req_id)
|
||||
cache.ongoing_prefetch.pop(req_id, None)
|
||||
logger.exception("Failed to release pending prefetch %s", handle.rid)
|
||||
cache.ongoing_prefetch.pop(handle, None)
|
||||
|
||||
for ack_id in list(cache.ongoing_backup):
|
||||
node_id, lock_params = cache.ongoing_backup.pop(ack_id)
|
||||
@@ -405,7 +405,7 @@ class StorageAttachment:
|
||||
except Exception:
|
||||
logger.exception("Failed to release host lock for backup op %s", ack_id)
|
||||
|
||||
for req_id in list(cache._storage_prefetch_hit_remaining_by_reqid):
|
||||
cache.discard_storage_prefetch_accounting(req_id)
|
||||
for handle in list(cache._storage_prefetch_hit_remaining_by_reqid):
|
||||
cache.discard_storage_prefetch_accounting(handle)
|
||||
cache.prefetch_loaded_tokens_by_reqid.clear()
|
||||
cache.prefetch_loaded_storage_start_by_reqid.clear()
|
||||
|
||||
@@ -18,6 +18,7 @@ from sglang.srt.mem_cache.allocator.page_interleave import (
|
||||
)
|
||||
from sglang.srt.mem_cache.base_prefix_cache import (
|
||||
BasePrefixCache,
|
||||
CacheRequestHandle,
|
||||
DecLockRefParams,
|
||||
DecLockRefResult,
|
||||
EvictParams,
|
||||
@@ -40,6 +41,7 @@ from sglang.srt.mem_cache.common import RetractionBackup
|
||||
from sglang.srt.mem_cache.hicache_storage import PoolName, PoolTransfer, SidecarPoolSpec
|
||||
from sglang.srt.mem_cache.hybrid_cache.hybrid_cache_controller import (
|
||||
HybridCacheController,
|
||||
PrefetchOperation,
|
||||
)
|
||||
from sglang.srt.mem_cache.memory_pool import MHATokenToKVPool
|
||||
from sglang.srt.mem_cache.radix_cache import RadixKey
|
||||
@@ -381,16 +383,18 @@ class UnifiedRadixCache(BasePrefixCache):
|
||||
self.ongoing_write_through: dict[int, _OngoingWriteThrough] = {}
|
||||
self.ongoing_load_back: dict[int, _OngoingLoadBack] = {}
|
||||
self.enable_storage = False
|
||||
self.prefetch_loaded_tokens_by_reqid: dict[str, int] = {}
|
||||
self.prefetch_loaded_storage_start_by_reqid: dict[str, int] = {}
|
||||
self.ongoing_prefetch: dict[str, _OngoingPrefetch] = {}
|
||||
self.prefetch_loaded_tokens_by_reqid: dict[CacheRequestHandle, int] = {}
|
||||
self.prefetch_loaded_storage_start_by_reqid: dict[CacheRequestHandle, int] = {}
|
||||
self.ongoing_prefetch: dict[CacheRequestHandle, _OngoingPrefetch] = {}
|
||||
# Rank-agreed L3-hit tokens not yet resolved as usable or unfulfilled.
|
||||
# Cache-mode entries survive L3->L2 until H2D succeeds or admission
|
||||
# fails; buffer-mode entries survive staging until the H2D ack.
|
||||
self._storage_prefetch_hit_remaining_by_reqid: dict[str, int] = {}
|
||||
# Rids whose storage prefetch resolved without a usable result;
|
||||
self._storage_prefetch_hit_remaining_by_reqid: dict[
|
||||
CacheRequestHandle, int
|
||||
] = {}
|
||||
# Attempts whose storage prefetch resolved without a usable result;
|
||||
# popped by the scheduler to pace availability-check retries.
|
||||
self._storage_prefetch_missed_rids: set[str] = set()
|
||||
self._storage_prefetch_missed_rids: set[CacheRequestHandle] = set()
|
||||
self.ongoing_backup: dict[int, tuple[NodeId, DecLockRefParams]] = {}
|
||||
if self.buffer_pipeline is not None:
|
||||
self.buffer_pipeline.reset()
|
||||
@@ -1881,12 +1885,8 @@ class UnifiedRadixCache(BasePrefixCache):
|
||||
if len(prefetch_key) < self.prefetch_threshold:
|
||||
return 0
|
||||
|
||||
from sglang.srt.mem_cache.hybrid_cache.hybrid_cache_controller import (
|
||||
PrefetchOperation,
|
||||
)
|
||||
|
||||
operation = PrefetchOperation(
|
||||
"__storage_hit_query__",
|
||||
CacheRequestHandle("__storage_hit_query__", 0),
|
||||
prefetch_key,
|
||||
last_hash,
|
||||
prefix_keys,
|
||||
@@ -1903,7 +1903,7 @@ class UnifiedRadixCache(BasePrefixCache):
|
||||
@rank_consensus(same_params=["req_id", "len(new_input_tokens)"])
|
||||
def prefetch_from_storage(
|
||||
self,
|
||||
req_id: str,
|
||||
request: CacheRequestHandle,
|
||||
last_host_node_id: NodeId,
|
||||
new_input_tokens: list[int],
|
||||
last_hash: Optional[str] = None,
|
||||
@@ -1943,16 +1943,16 @@ class UnifiedRadixCache(BasePrefixCache):
|
||||
stats["declined_too_short"] += 1
|
||||
# A too-short/fully-matched suffix can become a full recompute if
|
||||
# the device match evicts while queued; arm the paced retry.
|
||||
self._storage_prefetch_missed_rids.add(req_id)
|
||||
self._storage_prefetch_missed_rids.add(request)
|
||||
return
|
||||
if not buffer_mode and self.cache_controller.prefetch_rate_limited():
|
||||
stats["declined_rate_limited"] += 1
|
||||
self._storage_prefetch_missed_rids.add(req_id)
|
||||
self._storage_prefetch_missed_rids.add(request)
|
||||
return
|
||||
if req_id in self.ongoing_prefetch or (
|
||||
buffer_mode and self.buffer_pipeline.has_staged(req_id)
|
||||
if request in self.ongoing_prefetch or (
|
||||
buffer_mode and self.buffer_pipeline.has_staged(request)
|
||||
):
|
||||
# A fetch (or an unconsumed hold) already exists for this rid;
|
||||
# A fetch (or an unconsumed hold) already exists for this attempt;
|
||||
# overwriting would leak its staging slots.
|
||||
return
|
||||
|
||||
@@ -2009,13 +2009,13 @@ class UnifiedRadixCache(BasePrefixCache):
|
||||
if anchor_lock_params is not None:
|
||||
self.dec_host_lock_ref(last_host_node_id, anchor_lock_params)
|
||||
# Forfeited over transient staging pressure; retryable.
|
||||
self._storage_prefetch_missed_rids.add(req_id)
|
||||
self._storage_prefetch_missed_rids.add(request)
|
||||
return
|
||||
|
||||
aux_xfers = [x for xfers in comp_xfers.values() for x in xfers]
|
||||
aux_xfers.extend(sidecar_xfers)
|
||||
operation = self.cache_controller.prefetch(
|
||||
req_id,
|
||||
request,
|
||||
prefetch_key,
|
||||
last_hash,
|
||||
prefix_keys,
|
||||
@@ -2026,7 +2026,7 @@ class UnifiedRadixCache(BasePrefixCache):
|
||||
# rank-synchronized query outcome.
|
||||
operation.stats_requested_tokens = prefetch_length
|
||||
operation.storage_start = len(matched_prefix_tokens or [])
|
||||
self.ongoing_prefetch[req_id] = _OngoingPrefetch(
|
||||
self.ongoing_prefetch[request] = _OngoingPrefetch(
|
||||
last_host_node_id,
|
||||
prefetch_key,
|
||||
None,
|
||||
@@ -2036,7 +2036,7 @@ class UnifiedRadixCache(BasePrefixCache):
|
||||
)
|
||||
if buffer_mode:
|
||||
self.buffer_pipeline.set_prefix_ctx(
|
||||
req_id,
|
||||
request,
|
||||
matched_prefix_tokens,
|
||||
extra_key=extra_key,
|
||||
cache_salt=cache_salt,
|
||||
@@ -2044,7 +2044,7 @@ class UnifiedRadixCache(BasePrefixCache):
|
||||
# Pin the just-matched anchor now: deferred to IO commit it is
|
||||
# often already deleted under churn. The IO-commit call remains
|
||||
# as the second chance that decides the fetch's fate.
|
||||
self.buffer_pipeline.try_lock_anchor(req_id)
|
||||
self.buffer_pipeline.try_lock_anchor(request)
|
||||
else:
|
||||
# Cache mode reserves the requested span up front; buffer mode
|
||||
# grants occupancy later at hit-alloc time, sized to the hit.
|
||||
@@ -2081,12 +2081,15 @@ class UnifiedRadixCache(BasePrefixCache):
|
||||
else:
|
||||
return True
|
||||
|
||||
def has_ongoing_prefetch(self, handle: CacheRequestHandle) -> bool:
|
||||
return handle in self.ongoing_prefetch
|
||||
|
||||
@rank_consensus(same_params=True, same_results=True)
|
||||
def check_prefetch_progress(self, req_id: str) -> bool:
|
||||
if req_id not in self.ongoing_prefetch:
|
||||
def check_prefetch_progress(self, request: CacheRequestHandle) -> bool:
|
||||
if request not in self.ongoing_prefetch:
|
||||
return True
|
||||
|
||||
_, _, _, operation, _, _ = self.ongoing_prefetch[req_id]
|
||||
_, _, _, operation, _, _ = self.ongoing_prefetch[request]
|
||||
|
||||
# Determine whether or not we should terminate this prefetch request.
|
||||
should_terminate = operation.is_terminated() or self._can_terminate_prefetch(
|
||||
@@ -2098,8 +2101,8 @@ class UnifiedRadixCache(BasePrefixCache):
|
||||
|
||||
self.cache_controller.terminate_prefetch(operation)
|
||||
if operation.host_indices is None:
|
||||
self._storage_prefetch_missed_rids.add(req_id)
|
||||
self.revoke_pending_prefetch(req_id)
|
||||
self._storage_prefetch_missed_rids.add(request)
|
||||
self.revoke_pending_prefetch(request)
|
||||
else:
|
||||
self._handle_prefetch_result(operation)
|
||||
return True
|
||||
@@ -2112,7 +2115,7 @@ class UnifiedRadixCache(BasePrefixCache):
|
||||
# That is, when this function returns the host memory referenced must be inserted
|
||||
# into the radix tree or released to pool.
|
||||
|
||||
req_id = operation.request_id
|
||||
request = operation.handle
|
||||
completed_tokens = operation.completed_tokens
|
||||
hash_value = operation.hash_value
|
||||
|
||||
@@ -2123,12 +2126,12 @@ class UnifiedRadixCache(BasePrefixCache):
|
||||
_,
|
||||
anchor_lock_params,
|
||||
comp_xfers,
|
||||
) = self.ongoing_prefetch[req_id]
|
||||
) = self.ongoing_prefetch[request]
|
||||
|
||||
# All PP/TP ranks will get the same `min_completed_tokens`, because `completed_tokens`
|
||||
# and `pool_hits` in their operations are same. No need to sync cross-rank here.
|
||||
if not self._check_hybrid_prefetch_result(
|
||||
req_id,
|
||||
request,
|
||||
operation,
|
||||
completed_tokens,
|
||||
hash_value,
|
||||
@@ -2143,7 +2146,7 @@ class UnifiedRadixCache(BasePrefixCache):
|
||||
allocated_tokens = len(host_indices)
|
||||
if completed_tokens < allocated_tokens:
|
||||
self._resolve_storage_prefetch_tokens(
|
||||
req_id,
|
||||
request,
|
||||
allocated_tokens - completed_tokens,
|
||||
reason="storage_transfer",
|
||||
)
|
||||
@@ -2158,7 +2161,7 @@ class UnifiedRadixCache(BasePrefixCache):
|
||||
# No graft: release the rank-local tail beyond the synced usable
|
||||
# length, then park the bounce for admission-time consumption.
|
||||
return self.buffer_pipeline.stage_completed_prefetch(
|
||||
req_id, completed_tokens, hash_value
|
||||
request, completed_tokens, hash_value
|
||||
)
|
||||
|
||||
fetched_key = prefetch_key[:completed_tokens]
|
||||
@@ -2173,8 +2176,8 @@ class UnifiedRadixCache(BasePrefixCache):
|
||||
self._apply_cache_actions(insert_result.cache_actions)
|
||||
|
||||
if insert_result.host_insert_dropped:
|
||||
self._resolve_storage_prefetch_tokens(req_id, insert_result.prefix_len)
|
||||
self._finish_storage_prefetch(req_id, fulfilled_tokens=0, reason="dropped")
|
||||
self._resolve_storage_prefetch_tokens(request, insert_result.prefix_len)
|
||||
self._finish_storage_prefetch(request, fulfilled_tokens=0, reason="dropped")
|
||||
self.cache_controller.append_host_mem_release(
|
||||
host_indices=host_indices[:completed_tokens],
|
||||
extra_pools=[x for xfers in comp_xfers.values() for x in xfers],
|
||||
@@ -2201,23 +2204,23 @@ class UnifiedRadixCache(BasePrefixCache):
|
||||
# Cache mode has only completed L3 -> L2 here. Keep the usable
|
||||
# storage span unresolved until admission proves that L2 -> L1
|
||||
# load-back actually materialized it for this request.
|
||||
self._resolve_storage_prefetch_tokens(req_id, insert_result.prefix_len)
|
||||
self._resolve_storage_prefetch_tokens(request, insert_result.prefix_len)
|
||||
|
||||
self.dec_host_lock_ref(last_host_node_id, anchor_lock_params)
|
||||
del self.ongoing_prefetch[req_id]
|
||||
del self.ongoing_prefetch[request]
|
||||
self.cache_controller.prefetch_tokens_occupied -= len(prefetch_key)
|
||||
|
||||
self.prefetch_loaded_tokens_by_reqid[req_id] = loaded_from_storage
|
||||
self.prefetch_loaded_tokens_by_reqid[request] = loaded_from_storage
|
||||
if loaded_from_storage > 0:
|
||||
self.prefetch_loaded_storage_start_by_reqid[req_id] = (
|
||||
self.prefetch_loaded_storage_start_by_reqid[request] = (
|
||||
operation.storage_start + insert_result.prefix_len
|
||||
)
|
||||
else:
|
||||
self.prefetch_loaded_storage_start_by_reqid.pop(req_id, None)
|
||||
self.prefetch_loaded_storage_start_by_reqid.pop(request, None)
|
||||
logger.info(
|
||||
"HiCache prefetch %s req=%s completed=%d matched=%d loaded=%d occupied=%d",
|
||||
"dropped" if insert_result.host_insert_dropped else "success",
|
||||
req_id,
|
||||
request.rid,
|
||||
completed_tokens,
|
||||
insert_result.prefix_len,
|
||||
loaded_from_storage,
|
||||
@@ -2227,7 +2230,7 @@ class UnifiedRadixCache(BasePrefixCache):
|
||||
|
||||
def _check_hybrid_prefetch_result(
|
||||
self,
|
||||
req_id: str,
|
||||
request: CacheRequestHandle,
|
||||
operation: PrefetchOperation,
|
||||
completed_tokens: int,
|
||||
hash_value: list[str],
|
||||
@@ -2297,23 +2300,23 @@ class UnifiedRadixCache(BasePrefixCache):
|
||||
extra_pools=pool_transfers if operation.pool_transfers_done else None,
|
||||
)
|
||||
self._finish_storage_prefetch(
|
||||
req_id, fulfilled_tokens=0, reason="storage_transfer"
|
||||
request, fulfilled_tokens=0, reason="storage_transfer"
|
||||
)
|
||||
if anchor_lock_params is not None:
|
||||
self.dec_host_lock_ref(last_host_node_id, anchor_lock_params)
|
||||
if self.buffer_pipeline is not None:
|
||||
self.buffer_pipeline.pop_prefix_ctx(req_id)
|
||||
self.buffer_pipeline.release_anchor_lock(req_id)
|
||||
del self.ongoing_prefetch[req_id]
|
||||
self.buffer_pipeline.pop_prefix_ctx(request)
|
||||
self.buffer_pipeline.release_anchor_lock(request)
|
||||
del self.ongoing_prefetch[request]
|
||||
self.cache_controller.prefetch_tokens_occupied -= (
|
||||
self._prefetch_occupied_span(prefetch_key, host_indices)
|
||||
)
|
||||
self.prefetch_loaded_tokens_by_reqid[req_id] = 0
|
||||
self.prefetch_loaded_storage_start_by_reqid.pop(req_id, None)
|
||||
self.prefetch_loaded_tokens_by_reqid[request] = 0
|
||||
self.prefetch_loaded_storage_start_by_reqid.pop(request, None)
|
||||
logger.warning(
|
||||
"HiCache hybrid prefetch discarded req=%s completed=%d requested=%d "
|
||||
"kv_beliefs_kept_pages=%d",
|
||||
req_id,
|
||||
request.rid,
|
||||
completed_tokens,
|
||||
expected_tokens,
|
||||
keep_pages,
|
||||
@@ -2321,7 +2324,9 @@ class UnifiedRadixCache(BasePrefixCache):
|
||||
return False
|
||||
return True
|
||||
|
||||
def _record_storage_prefetch_hit(self, req_id: str, num_tokens: int) -> None:
|
||||
def _record_storage_prefetch_hit(
|
||||
self, request: CacheRequestHandle, num_tokens: int
|
||||
) -> None:
|
||||
"""Start accounting one rank-agreed positive L3 query result."""
|
||||
if (
|
||||
num_tokens <= 0
|
||||
@@ -2329,23 +2334,23 @@ class UnifiedRadixCache(BasePrefixCache):
|
||||
or self.storage_metrics_collector is None
|
||||
):
|
||||
return
|
||||
if req_id in self._storage_prefetch_hit_remaining_by_reqid:
|
||||
if request in self._storage_prefetch_hit_remaining_by_reqid:
|
||||
logger.warning(
|
||||
"Replacing unresolved storage-hit accounting req=%s old=%d new=%d",
|
||||
req_id,
|
||||
self._storage_prefetch_hit_remaining_by_reqid[req_id],
|
||||
request.rid,
|
||||
self._storage_prefetch_hit_remaining_by_reqid[request],
|
||||
num_tokens,
|
||||
)
|
||||
self.discard_storage_prefetch_accounting(req_id)
|
||||
self._storage_prefetch_hit_remaining_by_reqid[req_id] = num_tokens
|
||||
self.discard_storage_prefetch_accounting(request)
|
||||
self._storage_prefetch_hit_remaining_by_reqid[request] = num_tokens
|
||||
self.storage_metrics_collector.log_storage_prefetch_hit_tokens(num_tokens)
|
||||
|
||||
def _resolve_storage_prefetch_tokens(
|
||||
self, req_id: str, num_tokens: int, reason: Optional[str] = None
|
||||
self, request: CacheRequestHandle, num_tokens: int, reason: Optional[str] = None
|
||||
) -> None:
|
||||
if num_tokens <= 0:
|
||||
return
|
||||
remaining = self._storage_prefetch_hit_remaining_by_reqid.get(req_id)
|
||||
remaining = self._storage_prefetch_hit_remaining_by_reqid.get(request)
|
||||
if remaining is None:
|
||||
return
|
||||
dropped = min(num_tokens, remaining)
|
||||
@@ -2353,7 +2358,7 @@ class UnifiedRadixCache(BasePrefixCache):
|
||||
logger.warning(
|
||||
"Storage-prefetch accounting exceeded remaining "
|
||||
"tokens req=%s requested=%d remaining=%d reason=%s",
|
||||
req_id,
|
||||
request.rid,
|
||||
num_tokens,
|
||||
remaining,
|
||||
reason,
|
||||
@@ -2364,21 +2369,21 @@ class UnifiedRadixCache(BasePrefixCache):
|
||||
)
|
||||
remaining -= dropped
|
||||
if remaining:
|
||||
self._storage_prefetch_hit_remaining_by_reqid[req_id] = remaining
|
||||
self._storage_prefetch_hit_remaining_by_reqid[request] = remaining
|
||||
else:
|
||||
self._storage_prefetch_hit_remaining_by_reqid.pop(req_id, None)
|
||||
self._storage_prefetch_hit_remaining_by_reqid.pop(request, None)
|
||||
|
||||
def _finish_storage_prefetch(
|
||||
self, req_id: str, fulfilled_tokens: int, reason: Optional[str]
|
||||
self, request: CacheRequestHandle, fulfilled_tokens: int, reason: Optional[str]
|
||||
) -> None:
|
||||
remaining = self._storage_prefetch_hit_remaining_by_reqid.pop(req_id, None)
|
||||
remaining = self._storage_prefetch_hit_remaining_by_reqid.pop(request, None)
|
||||
if remaining is None:
|
||||
return
|
||||
if fulfilled_tokens > remaining:
|
||||
logger.warning(
|
||||
"Storage-prefetch fulfilled accounting exceeded remaining "
|
||||
"tokens req=%s fulfilled=%d remaining=%d",
|
||||
req_id,
|
||||
request.rid,
|
||||
fulfilled_tokens,
|
||||
remaining,
|
||||
)
|
||||
@@ -2389,7 +2394,7 @@ class UnifiedRadixCache(BasePrefixCache):
|
||||
)
|
||||
|
||||
def finish_storage_prefetch_admission(
|
||||
self, req_id: str, fulfilled_tokens: int, reason: Optional[str]
|
||||
self, request: CacheRequestHandle, fulfilled_tokens: int, reason: Optional[str]
|
||||
) -> None:
|
||||
"""Resolve a cache-mode L3 hit after request admission.
|
||||
|
||||
@@ -2397,70 +2402,72 @@ class UnifiedRadixCache(BasePrefixCache):
|
||||
host allocation is transport staging rather than a resident L2 hit.
|
||||
"""
|
||||
if self.host_memory_mode == "cache":
|
||||
self._finish_storage_prefetch(req_id, fulfilled_tokens, reason)
|
||||
self._finish_storage_prefetch(request, fulfilled_tokens, reason)
|
||||
|
||||
def discard_storage_prefetch_accounting(self, req_id: str) -> None:
|
||||
def discard_storage_prefetch_accounting(self, request: CacheRequestHandle) -> None:
|
||||
"""Drop lifecycle state for cases intentionally excluded from metrics."""
|
||||
self._storage_prefetch_hit_remaining_by_reqid.pop(req_id, None)
|
||||
self._storage_prefetch_hit_remaining_by_reqid.pop(request, None)
|
||||
|
||||
def _handle_storage_prefetch_anchor_loss(self, req_id: str) -> None:
|
||||
self._finish_storage_prefetch(req_id, fulfilled_tokens=0, reason="shrunk")
|
||||
def _handle_storage_prefetch_anchor_loss(self, request: CacheRequestHandle) -> None:
|
||||
self._finish_storage_prefetch(request, fulfilled_tokens=0, reason="shrunk")
|
||||
# The span is still L3-resident; retry from the shorter live match.
|
||||
self._storage_prefetch_missed_rids.add(req_id)
|
||||
self.revoke_pending_prefetch(req_id)
|
||||
self._storage_prefetch_missed_rids.add(request)
|
||||
self.revoke_pending_prefetch(request)
|
||||
|
||||
def pop_prefetch_loaded_tokens(self, req_id: str) -> int:
|
||||
def pop_prefetch_loaded_tokens(self, request: CacheRequestHandle) -> int:
|
||||
# The request is being scheduled; a still-unserved miss marker is moot.
|
||||
self._storage_prefetch_missed_rids.discard(req_id)
|
||||
self.prefetch_loaded_storage_start_by_reqid.pop(req_id, None)
|
||||
return self.prefetch_loaded_tokens_by_reqid.pop(req_id, 0)
|
||||
self._storage_prefetch_missed_rids.discard(request)
|
||||
self.prefetch_loaded_storage_start_by_reqid.pop(request, None)
|
||||
return self.prefetch_loaded_tokens_by_reqid.pop(request, 0)
|
||||
|
||||
def pop_prefetch_loaded_span(self, req_id: str) -> tuple[int, Optional[int]]:
|
||||
def pop_prefetch_loaded_span(
|
||||
self, request: CacheRequestHandle
|
||||
) -> tuple[int, Optional[int]]:
|
||||
"""Pop the loaded L3 token count and its absolute prefix start."""
|
||||
self._storage_prefetch_missed_rids.discard(req_id)
|
||||
self._storage_prefetch_missed_rids.discard(request)
|
||||
return (
|
||||
self.prefetch_loaded_tokens_by_reqid.pop(req_id, 0),
|
||||
self.prefetch_loaded_storage_start_by_reqid.pop(req_id, None),
|
||||
self.prefetch_loaded_tokens_by_reqid.pop(request, 0),
|
||||
self.prefetch_loaded_storage_start_by_reqid.pop(request, None),
|
||||
)
|
||||
|
||||
def pop_storage_prefetch_miss(self, req_id: str) -> bool:
|
||||
def pop_storage_prefetch_miss(self, request: CacheRequestHandle) -> bool:
|
||||
"""True once per resolved storage-prefetch miss for a live request;
|
||||
the scheduler uses it to arm the paced availability-check retry."""
|
||||
if req_id in self._storage_prefetch_missed_rids:
|
||||
self._storage_prefetch_missed_rids.discard(req_id)
|
||||
if request in self._storage_prefetch_missed_rids:
|
||||
self._storage_prefetch_missed_rids.discard(request)
|
||||
return True
|
||||
return False
|
||||
|
||||
def plan_staged_splice(
|
||||
self, req_id: str, device_prefix_len: int
|
||||
self, request: CacheRequestHandle, device_prefix_len: int
|
||||
) -> tuple[int, int]:
|
||||
"""(kv, swa) host-hit tokens a staged buffer-mode prefetch will splice
|
||||
given the request's live device prefix; frees unusable holds."""
|
||||
if self.buffer_pipeline is None:
|
||||
return 0, 0
|
||||
return self.buffer_pipeline.plan_staged_splice(req_id, device_prefix_len)
|
||||
return self.buffer_pipeline.plan_staged_splice(request, device_prefix_len)
|
||||
|
||||
def staged_prefetch_swa_tokens(self, req_id: str) -> int:
|
||||
def staged_prefetch_swa_tokens(self, request: CacheRequestHandle) -> int:
|
||||
"""SWA device tokens consuming a staged buffer-mode prefetch will
|
||||
allocate; surfaced as the request's swa_host_hit_length."""
|
||||
if self.buffer_pipeline is None:
|
||||
return 0
|
||||
return self.buffer_pipeline.staged_prefetch_swa_tokens(req_id)
|
||||
return self.buffer_pipeline.staged_prefetch_swa_tokens(request)
|
||||
|
||||
@rank_consensus(same_params=True)
|
||||
def release_aborted_request(self, rid: str) -> None:
|
||||
def release_aborted_request(self, request: CacheRequestHandle) -> None:
|
||||
if self.linker is not None:
|
||||
self.linker.release_request(rid)
|
||||
self.prefetch_loaded_tokens_by_reqid.pop(rid, None)
|
||||
self.prefetch_loaded_storage_start_by_reqid.pop(rid, None)
|
||||
self._storage_prefetch_missed_rids.discard(rid)
|
||||
self.linker.release_request(request.rid)
|
||||
self.prefetch_loaded_tokens_by_reqid.pop(request, None)
|
||||
self.prefetch_loaded_storage_start_by_reqid.pop(request, None)
|
||||
self._storage_prefetch_missed_rids.discard(request)
|
||||
if (
|
||||
self.buffer_pipeline is not None
|
||||
and self.buffer_pipeline.release_staged_hold(rid)
|
||||
and self.buffer_pipeline.release_staged_hold(request)
|
||||
):
|
||||
return
|
||||
self.discard_storage_prefetch_accounting(rid)
|
||||
if rid not in self.ongoing_prefetch:
|
||||
self.discard_storage_prefetch_accounting(request)
|
||||
if request not in self.ongoing_prefetch:
|
||||
return
|
||||
|
||||
(
|
||||
@@ -2470,19 +2477,19 @@ class UnifiedRadixCache(BasePrefixCache):
|
||||
operation,
|
||||
anchor_lock_params,
|
||||
comp_xfers,
|
||||
) = self.ongoing_prefetch[rid]
|
||||
) = self.ongoing_prefetch[request]
|
||||
if operation.host_indices is None:
|
||||
self.cache_controller.terminate_prefetch(operation)
|
||||
self.revoke_pending_prefetch(rid)
|
||||
self.revoke_pending_prefetch(request)
|
||||
return
|
||||
|
||||
completed_tokens, _ = self.cache_controller.terminate_prefetch(operation)
|
||||
if anchor_lock_params is not None:
|
||||
self.dec_host_lock_ref(last_host_node_id, anchor_lock_params)
|
||||
del self.ongoing_prefetch[rid]
|
||||
del self.ongoing_prefetch[request]
|
||||
if self.buffer_pipeline is not None:
|
||||
self.buffer_pipeline.pop_prefix_ctx(rid)
|
||||
self.buffer_pipeline.release_anchor_lock(rid)
|
||||
self.buffer_pipeline.pop_prefix_ctx(request)
|
||||
self.buffer_pipeline.release_anchor_lock(request)
|
||||
pool_transfers = [x for xfers in comp_xfers.values() for x in xfers]
|
||||
self.cache_controller.append_host_mem_release(
|
||||
host_indices=host_indices[:completed_tokens],
|
||||
@@ -2536,9 +2543,9 @@ class UnifiedRadixCache(BasePrefixCache):
|
||||
return len(host_indices) if host_indices is not None else 0
|
||||
return len(prefetch_key)
|
||||
|
||||
def revoke_pending_prefetch(self, req_id: str) -> None:
|
||||
info = self.ongoing_prefetch.pop(req_id, None)
|
||||
self._finish_storage_prefetch(req_id, fulfilled_tokens=0, reason="dropped")
|
||||
def revoke_pending_prefetch(self, request: CacheRequestHandle) -> None:
|
||||
info = self.ongoing_prefetch.pop(request, None)
|
||||
self._finish_storage_prefetch(request, fulfilled_tokens=0, reason="dropped")
|
||||
if info is None:
|
||||
return
|
||||
(
|
||||
@@ -2551,8 +2558,8 @@ class UnifiedRadixCache(BasePrefixCache):
|
||||
) = info
|
||||
self._invalidate_absent_from_hit_query(operation)
|
||||
if self.buffer_pipeline is not None:
|
||||
self.buffer_pipeline.pop_prefix_ctx(req_id)
|
||||
self.buffer_pipeline.release_anchor_lock(req_id)
|
||||
self.buffer_pipeline.pop_prefix_ctx(request)
|
||||
self.buffer_pipeline.release_anchor_lock(request)
|
||||
cc = self.cache_controller
|
||||
cc.append_host_mem_release(
|
||||
extra_pools=[x for xfers in comp_xfers.values() for x in xfers]
|
||||
@@ -2606,13 +2613,13 @@ class UnifiedRadixCache(BasePrefixCache):
|
||||
"""Allocate the hit-sized bounce and launch the transfer.
|
||||
Returns False when staging pressure defers the allocation
|
||||
(buffer mode parks and retries; cache mode revokes)."""
|
||||
req_id = operation.request_id
|
||||
info = self.ongoing_prefetch.get(req_id)
|
||||
request = operation.handle
|
||||
info = self.ongoing_prefetch.get(request)
|
||||
hit_tokens = operation.storage_hit_count
|
||||
if info is None:
|
||||
return True # aborted/cleaned; nothing to retry
|
||||
if operation.is_terminated():
|
||||
self.revoke_pending_prefetch(req_id)
|
||||
self.revoke_pending_prefetch(request)
|
||||
return True
|
||||
|
||||
if buffer_mode and cc.prefetch_rate_limited():
|
||||
@@ -2624,20 +2631,20 @@ class UnifiedRadixCache(BasePrefixCache):
|
||||
# IO commit: pin before the bounce alloc so a cancel is a
|
||||
# plain revoke and a parked op keeps its pin; a fetch whose
|
||||
# splice base is gone is not worth its storage read.
|
||||
if self.buffer_pipeline.try_lock_anchor(req_id) == "anchor_lost":
|
||||
if self.buffer_pipeline.try_lock_anchor(request) == "anchor_lost":
|
||||
self._prefetch_outcome_stats["declined_anchor_lost"] += 1
|
||||
self._handle_storage_prefetch_anchor_loss(req_id)
|
||||
self._handle_storage_prefetch_anchor_loss(request)
|
||||
return True
|
||||
if self.buffer_pipeline.staged_span_covered(
|
||||
req_id, operation.storage_hit_count
|
||||
request, operation.storage_hit_count
|
||||
):
|
||||
# Live tree already covers the span: nothing left to
|
||||
# splice, so skip the storage read.
|
||||
self._prefetch_outcome_stats["declined_device_covered"] += 1
|
||||
self._finish_storage_prefetch(
|
||||
req_id, fulfilled_tokens=0, reason=None
|
||||
request, fulfilled_tokens=0, reason=None
|
||||
)
|
||||
self.revoke_pending_prefetch(req_id)
|
||||
self.revoke_pending_prefetch(request)
|
||||
return True
|
||||
alloc_len = hit_tokens
|
||||
host_indices = cc.mem_pool_host.alloc(alloc_len)
|
||||
@@ -2658,18 +2665,18 @@ class UnifiedRadixCache(BasePrefixCache):
|
||||
if buffer_mode:
|
||||
return False
|
||||
self._finish_storage_prefetch(
|
||||
req_id, fulfilled_tokens=0, reason="host_capacity"
|
||||
request, fulfilled_tokens=0, reason="host_capacity"
|
||||
)
|
||||
self.revoke_pending_prefetch(req_id)
|
||||
self.revoke_pending_prefetch(request)
|
||||
return True
|
||||
|
||||
self._resolve_storage_prefetch_tokens(
|
||||
req_id, hit_tokens - alloc_len, reason="host_capacity"
|
||||
request, hit_tokens - alloc_len, reason="host_capacity"
|
||||
)
|
||||
operation.storage_hit_count = alloc_len
|
||||
operation.hash_value = operation.hash_value[: alloc_len // self.page_size]
|
||||
operation.host_indices = host_indices
|
||||
self.ongoing_prefetch[req_id] = info._replace(host_indices=host_indices)
|
||||
self.ongoing_prefetch[request] = info._replace(host_indices=host_indices)
|
||||
if buffer_mode:
|
||||
cc.prefetch_tokens_occupied += alloc_len
|
||||
cc.prefetch_buffer.put(operation)
|
||||
@@ -2684,24 +2691,24 @@ class UnifiedRadixCache(BasePrefixCache):
|
||||
break
|
||||
parked.popleft()
|
||||
for operation in _drain_queue(cc.prefetch_hit_queue, n_storage_hit):
|
||||
req_id = operation.request_id
|
||||
request = operation.handle
|
||||
hit_tokens = operation.storage_hit_count
|
||||
info = self.ongoing_prefetch.get(req_id)
|
||||
info = self.ongoing_prefetch.get(request)
|
||||
if info is None:
|
||||
# Request already aborted/cleaned up; still flush the
|
||||
# query's absent-hash feedback.
|
||||
self._invalidate_absent_from_hit_query(operation)
|
||||
if hit_tokens > 0:
|
||||
self.discard_storage_prefetch_accounting(req_id)
|
||||
self.discard_storage_prefetch_accounting(request)
|
||||
continue
|
||||
if hit_tokens > 0:
|
||||
self._record_storage_prefetch_hit(req_id, hit_tokens)
|
||||
self._record_storage_prefetch_hit(request, hit_tokens)
|
||||
if operation.is_terminated():
|
||||
# Controller-side miss termination (retryable) or an abort
|
||||
# race (abort cleanup discards the marker).
|
||||
if hit_tokens > 0:
|
||||
self._finish_storage_prefetch(
|
||||
req_id,
|
||||
request,
|
||||
fulfilled_tokens=0,
|
||||
reason=(
|
||||
"below_threshold"
|
||||
@@ -2709,17 +2716,17 @@ class UnifiedRadixCache(BasePrefixCache):
|
||||
else None
|
||||
),
|
||||
)
|
||||
self._storage_prefetch_missed_rids.add(req_id)
|
||||
self.revoke_pending_prefetch(req_id)
|
||||
self._storage_prefetch_missed_rids.add(request)
|
||||
self.revoke_pending_prefetch(request)
|
||||
continue
|
||||
if hit_tokens < self.prefetch_threshold:
|
||||
# Below-threshold hits are not worth the transfer.
|
||||
self._account_prefetch_outcome(operation, revoked=True)
|
||||
self._finish_storage_prefetch(
|
||||
req_id, fulfilled_tokens=0, reason="below_threshold"
|
||||
request, fulfilled_tokens=0, reason="below_threshold"
|
||||
)
|
||||
self._storage_prefetch_missed_rids.add(req_id)
|
||||
self.revoke_pending_prefetch(req_id)
|
||||
self._storage_prefetch_missed_rids.add(request)
|
||||
self.revoke_pending_prefetch(request)
|
||||
continue
|
||||
self._invalidate_absent_from_hit_query(operation)
|
||||
self._account_prefetch_outcome(operation, revoked=False)
|
||||
@@ -2732,18 +2739,18 @@ class UnifiedRadixCache(BasePrefixCache):
|
||||
for ack in _drain_queue(cc.ack_prefetch_queue, n_ack_prefetch):
|
||||
operation = ack.operation
|
||||
if ack.completed_tokens is not None:
|
||||
if operation.request_id in self.ongoing_prefetch:
|
||||
if operation.handle in self.ongoing_prefetch:
|
||||
assert operation.completed_tokens <= ack.completed_tokens
|
||||
operation.completed_tokens = ack.completed_tokens
|
||||
if ack.pool_hits is not None:
|
||||
if operation.request_id in self.ongoing_prefetch:
|
||||
if operation.handle in self.ongoing_prefetch:
|
||||
operation.pool_storage_result.update_extra_pool_hit_pages(
|
||||
ack.pool_hits
|
||||
)
|
||||
operation.pool_transfers_done = True
|
||||
if ack.completed_req:
|
||||
if operation.request_id in self.ongoing_prefetch:
|
||||
# check_prefetch_progress() is not called for this rid yet.
|
||||
if operation.handle in self.ongoing_prefetch:
|
||||
# check_prefetch_progress() is not called for this attempt yet.
|
||||
# Let us insert the prefetch result into the radix tree.
|
||||
self._handle_prefetch_result(operation)
|
||||
cc.append_host_mem_release(
|
||||
|
||||
@@ -9,6 +9,8 @@ import torch
|
||||
from sglang.srt.managers.schedule_batch import ReqKvInfo
|
||||
from sglang.srt.mem_cache.base_prefix_cache import (
|
||||
BasePrefixCache,
|
||||
CacheRequestHandle,
|
||||
CacheRequestOutcome,
|
||||
DecLockRefParams,
|
||||
DecLockRefResult,
|
||||
EvictParams,
|
||||
@@ -360,6 +362,12 @@ class StreamingSession(BasePrefixCache):
|
||||
return
|
||||
self.inner.cache_unfinished_req(req, **kwargs)
|
||||
|
||||
def finish(self, handle: CacheRequestHandle, outcome: CacheRequestOutcome) -> None:
|
||||
self.inner.finish(handle, outcome)
|
||||
|
||||
def release_aborted_request(self, handle: CacheRequestHandle) -> None:
|
||||
self.inner.release_aborted_request(handle)
|
||||
|
||||
def evict(self, params: EvictParams) -> EvictResult:
|
||||
return self.inner.evict(params)
|
||||
|
||||
@@ -564,16 +572,18 @@ class StreamingSession(BasePrefixCache):
|
||||
def init_load_back(self, params: InitLoadBackParams):
|
||||
return self.inner.init_load_back(params)
|
||||
|
||||
def pop_prefetch_loaded_span(self, req_id: str) -> tuple[int, Optional[int]]:
|
||||
return self.inner.pop_prefetch_loaded_span(req_id)
|
||||
def pop_prefetch_loaded_span(
|
||||
self, handle: CacheRequestHandle
|
||||
) -> tuple[int, Optional[int]]:
|
||||
return self.inner.pop_prefetch_loaded_span(handle)
|
||||
|
||||
def finish_storage_prefetch_admission(
|
||||
self, req_id: str, fulfilled_tokens: int, reason: Optional[str]
|
||||
self, handle: CacheRequestHandle, fulfilled_tokens: int, reason: Optional[str]
|
||||
) -> None:
|
||||
self.inner.finish_storage_prefetch_admission(req_id, fulfilled_tokens, reason)
|
||||
self.inner.finish_storage_prefetch_admission(handle, fulfilled_tokens, reason)
|
||||
|
||||
def discard_storage_prefetch_accounting(self, req_id: str) -> None:
|
||||
self.inner.discard_storage_prefetch_accounting(req_id)
|
||||
def discard_storage_prefetch_accounting(self, handle: CacheRequestHandle) -> None:
|
||||
self.inner.discard_storage_prefetch_accounting(handle)
|
||||
|
||||
def ready_to_load_host_cache(self):
|
||||
return self.inner.ready_to_load_host_cache()
|
||||
|
||||
Reference in New Issue
Block a user