Stabilize CP HiCache page-tail ownership under EAGLE reuse
CP shared KV and HiCache now keep page-aligned physical ownership while preserving valid-token radix semantics. Repeated tiny EAGLE exact hits free duplicate tail pages instead of leaking one allocator page, owner-lane load-back uses page-vector admission/eviction, and single-DP idle schedulers avoid entering an unnecessary MLP-sync collective. The commit also records the current page-aligned cache contract and adds gated decode-side EAGLE accept diagnostics so future accept-length collapses can be tied to draft KV/state transfer evidence instead of more prefill cache speculation. Constraint: CP HiCache allocator ownership is page-granular while radix matching remains valid-token based. Constraint: New diagnostics must be gated and must not alter normal EAGLE, transfer, or cache behavior. Rejected: Padding short requests to cp_size or 2*cp_size pages | wastes KV capacity and still hides valid-tail lifecycle bugs. Rejected: Adding more unconditional collectives to prove CP consistency | hot-path collectives previously caused severe performance risk. Confidence: medium Scope-risk: broad Directive: Do not reintroduce silent fallback for CP shared KV/HiCache paths; warning-level fallback or fail-fast is intentional. Tested: git diff --check Tested: local py_compile for all modified Python files Tested: remote g0034 container py_compile for modified Python/test files Tested: remote g0034 container PYTHONPATH=python python -m pytest -q test/registered/unit/layers/test_nsa_cp_utils.py test/registered/unit/mem_cache/test_cp_shared_kv_runtime.py test/registered/unit/mem_cache/test_cp_hicache_load_back_owner_lanes.py test/registered/unit/managers/test_scheduler_dp_attn_mixin.py => 114 passed, 5 warnings, 2 subtests passed Not-tested: full ETE traffic rerun after this commit Not-tested: CUDA/TAI kernel benchmark coverage for all production shapes
This commit is contained in:
@@ -509,6 +509,22 @@ class DecodePreallocQueue:
|
||||
len(kv_args.state_data_ptrs),
|
||||
)
|
||||
|
||||
if envs.SGLANG_EAGLE_ACCEPT_DEBUG.get() and self.spec_algorithm.is_eagle():
|
||||
logger.info(
|
||||
"[EAGLE_ACCEPT_DEBUG] decode_kv_manager cp_rank=%s "
|
||||
"target_kv_bufs=%s draft_kv_bufs=%s total_kv_bufs=%s "
|
||||
"target_state_type=%s registered_state_bufs=%s "
|
||||
"draft_state_type=%s draft_state_bufs=%s",
|
||||
self.tp_rank,
|
||||
target_kv_buffer_count,
|
||||
draft_kv_buffer_count,
|
||||
len(kv_args.kv_data_ptrs),
|
||||
kv_args.state_type,
|
||||
len(kv_args.state_data_ptrs),
|
||||
kv_args.draft_state_type,
|
||||
kv_args.draft_state_buffer_count,
|
||||
)
|
||||
|
||||
kv_args.ib_device = self.scheduler.server_args.disaggregation_ib_device
|
||||
kv_args.gpu_id = self.scheduler.gpu_id
|
||||
kv_manager_class = get_kv_class(self.transfer_backend, KVClassType.MANAGER)
|
||||
|
||||
@@ -350,6 +350,22 @@ class PrefillBootstrapQueue:
|
||||
len(kv_args.state_data_ptrs),
|
||||
)
|
||||
|
||||
if envs.SGLANG_EAGLE_ACCEPT_DEBUG.get() and self.spec_algorithm.is_eagle():
|
||||
logger.info(
|
||||
"[EAGLE_ACCEPT_DEBUG] prefill_kv_manager cp_rank=%s "
|
||||
"target_kv_bufs=%s draft_kv_bufs=%s total_kv_bufs=%s "
|
||||
"target_state_type=%s registered_state_bufs=%s "
|
||||
"draft_state_type=%s draft_state_bufs=%s",
|
||||
self.tp_rank,
|
||||
target_kv_buffer_count,
|
||||
draft_kv_buffer_count,
|
||||
len(kv_args.kv_data_ptrs),
|
||||
kv_args.state_type,
|
||||
len(kv_args.state_data_ptrs),
|
||||
kv_args.draft_state_type,
|
||||
kv_args.draft_state_buffer_count,
|
||||
)
|
||||
|
||||
kv_manager_class = get_kv_class(self.transfer_backend, KVClassType.MANAGER)
|
||||
kv_manager = kv_manager_class(
|
||||
kv_args,
|
||||
|
||||
@@ -217,6 +217,8 @@ class Envs:
|
||||
SGLANG_CP_SHARED_KV_MLA_PREFETCH_MIN_EXTEND_TOKENS = EnvInt(-1)
|
||||
SGLANG_CP_DRAFT_SHARED_KV = EnvBool(False)
|
||||
SGLANG_CP_DRAFT_SHARED_KV_DEBUG = EnvBool(False)
|
||||
SGLANG_EAGLE_ACCEPT_DEBUG = EnvBool(False)
|
||||
SGLANG_EAGLE_ACCEPT_DEBUG_INTERVAL = EnvInt(128)
|
||||
SGLANG_DISABLE_TAI_BIGRAM = EnvBool(False)
|
||||
SGLANG_TEST_REQUEST_TIME_STATS = EnvBool(False)
|
||||
SGLANG_DISABLE_TP_MEMORY_INBALANCE_CHECK = EnvBool(False)
|
||||
|
||||
@@ -968,7 +968,11 @@ def can_reuse_current_extend_kv(forward_batch) -> bool:
|
||||
seq_len = int(seq_lens_cpu[0].item())
|
||||
if extend_len <= 0 or seq_len < extend_len:
|
||||
return False
|
||||
return int(out_cache_loc.numel()) == extend_len
|
||||
# ForwardBatch pads tensors such as out_cache_loc at the tail for CUDA graph
|
||||
# and CP alignment. The first extend_len rows still cover the valid current
|
||||
# suffix, so padded batches remain eligible for current reuse as long as the
|
||||
# valid suffix is fully present.
|
||||
return int(out_cache_loc.numel()) >= extend_len
|
||||
|
||||
|
||||
def should_reuse_current_extend_kv(forward_batch) -> bool:
|
||||
|
||||
@@ -403,6 +403,51 @@ def should_use_replicated_compute_for_short_radix_hit(
|
||||
return False
|
||||
|
||||
|
||||
def should_skip_cp_shared_kv_cp_split_for_short_page_extent(
|
||||
forward_batch: "ForwardBatch",
|
||||
cp_size: int,
|
||||
) -> bool:
|
||||
"""Avoid in-seq CP split when a shared-KV suffix has too few pages.
|
||||
|
||||
CP shared KV is page-owned. A cache-hit suffix with fewer physical pages
|
||||
than CP lanes creates mostly-zero in-seq segments; the distributed NSA path
|
||||
has repeatedly shown hangs on that shape. Keep the page cache contract by
|
||||
running those tiny suffixes without NSA in-seq CP split instead of falling
|
||||
back to token-balanced page-splitting.
|
||||
"""
|
||||
|
||||
if (
|
||||
forward_batch is None
|
||||
or not getattr(forward_batch, "uses_cp_shared_kv", False)
|
||||
or cp_size <= 1
|
||||
):
|
||||
return False
|
||||
|
||||
extend_seq_lens_cpu = getattr(forward_batch, "extend_seq_lens_cpu", None)
|
||||
extend_prefix_lens_cpu = getattr(forward_batch, "extend_prefix_lens_cpu", None)
|
||||
token_to_kv_pool = getattr(forward_batch, "token_to_kv_pool", None)
|
||||
page_size = int(getattr(token_to_kv_pool, "page_size", 0) or 0)
|
||||
if (
|
||||
extend_seq_lens_cpu is None
|
||||
or len(extend_seq_lens_cpu) != 1
|
||||
or extend_prefix_lens_cpu is None
|
||||
or len(extend_prefix_lens_cpu) != 1
|
||||
or page_size <= 0
|
||||
):
|
||||
return False
|
||||
|
||||
extend_len = int(extend_seq_lens_cpu[0])
|
||||
if extend_len <= 0:
|
||||
return False
|
||||
|
||||
prefix_len = int(extend_prefix_lens_cpu[0])
|
||||
if prefix_len <= 0 or prefix_len % page_size != 0:
|
||||
return False
|
||||
|
||||
padded_pages = ceil_div(extend_len, page_size)
|
||||
return padded_pages < cp_size
|
||||
|
||||
|
||||
def can_cp_split(seq_len: int, cp_size: int, use_nsa: bool, forward_batch):
|
||||
if is_nsa_prefill_cp_round_robin_split():
|
||||
cur_cp_seq_len = seq_len // cp_size
|
||||
@@ -415,6 +460,10 @@ def can_cp_split(seq_len: int, cp_size: int, use_nsa: bool, forward_batch):
|
||||
# the seq data needs to be divided and recombined at twice the size of cp_size.
|
||||
if should_use_replicated_compute_for_short_radix_hit(forward_batch, cp_size):
|
||||
return False
|
||||
if should_skip_cp_shared_kv_cp_split_for_short_page_extent(
|
||||
forward_batch, cp_size
|
||||
):
|
||||
return False
|
||||
cur_cp_seq_len = seq_len // (cp_size * 2)
|
||||
if (
|
||||
cur_cp_seq_len != 0
|
||||
|
||||
@@ -1782,6 +1782,7 @@ class NativeSparseAttnBackend(
|
||||
)
|
||||
if can_reuse_current_kv:
|
||||
current_kv_cache = _cat([k, k_rope], dim=-1)
|
||||
current_locs_for_reuse = forward_batch.out_cache_loc
|
||||
logical_page_table_1 = page_table_1
|
||||
current_remap_page_size, current_remap_logical_page_capacity = (
|
||||
current_loc_remap_fast_path_args(forward_batch)
|
||||
@@ -1809,13 +1810,30 @@ class NativeSparseAttnBackend(
|
||||
"MLA current reuse cp_rank=%s layer=%s current_locs=%s remapped=%s kv_ck=%s rope_ck=%s",
|
||||
forward_batch.cp_shared_kv_layout.cp_rank,
|
||||
layer.layer_id,
|
||||
tensor_debug_summary(forward_batch.out_cache_loc),
|
||||
tensor_debug_summary(current_locs_for_reuse),
|
||||
tensor_debug_summary(page_table_1),
|
||||
tensor_debug_checksum(k),
|
||||
tensor_debug_checksum(k_rope),
|
||||
)
|
||||
kv_cache = current_kv_cache
|
||||
else:
|
||||
extend_lens_cpu_for_current = getattr(
|
||||
forward_batch, "extend_seq_lens_cpu", None
|
||||
)
|
||||
if (
|
||||
extend_lens_cpu_for_current is not None
|
||||
and len(extend_lens_cpu_for_current) == 1
|
||||
):
|
||||
valid_current_rows = int(extend_lens_cpu_for_current[0])
|
||||
if (
|
||||
valid_current_rows > 0
|
||||
and valid_current_rows < int(current_locs_for_reuse.numel())
|
||||
and valid_current_rows <= int(current_kv_cache.shape[0])
|
||||
):
|
||||
current_kv_cache = current_kv_cache[:valid_current_rows]
|
||||
current_locs_for_reuse = current_locs_for_reuse[
|
||||
:valid_current_rows
|
||||
]
|
||||
prefetched_kv = None
|
||||
if mla_prefetcher is not None:
|
||||
prefetched_kv = mla_prefetcher.consume_prefix_with_current(
|
||||
@@ -1823,7 +1841,7 @@ class NativeSparseAttnBackend(
|
||||
kv_cache=kv_cache,
|
||||
logical_locs=logical_page_table_1,
|
||||
current_kv_cache=current_kv_cache,
|
||||
current_locs=forward_batch.out_cache_loc,
|
||||
current_locs=current_locs_for_reuse,
|
||||
current_remap_page_size=current_remap_page_size,
|
||||
current_remap_logical_page_capacity=current_remap_logical_page_capacity,
|
||||
)
|
||||
@@ -1869,7 +1887,7 @@ class NativeSparseAttnBackend(
|
||||
f"extend_lens={extend_lens} "
|
||||
f"current_rows={int(current_kv_cache.shape[0])} "
|
||||
f"logical_page_table_shape={tuple(logical_page_table_1.shape)} "
|
||||
f"current_locs_shape={tuple(forward_batch.out_cache_loc.shape)} "
|
||||
f"current_locs_shape={tuple(current_locs_for_reuse.shape)} "
|
||||
f"page_size={page_size}"
|
||||
)
|
||||
prefix_pages = int(prefix_lens_cpu[0]) // page_size
|
||||
@@ -1885,7 +1903,7 @@ class NativeSparseAttnBackend(
|
||||
kv_cache=kv_cache,
|
||||
logical_locs=logical_page_table_1,
|
||||
current_kv_cache=current_kv_cache,
|
||||
current_locs=forward_batch.out_cache_loc,
|
||||
current_locs=current_locs_for_reuse,
|
||||
slot_remap=slot_remap,
|
||||
layout=forward_batch.cp_shared_kv_layout,
|
||||
page_size=page_size,
|
||||
@@ -1946,7 +1964,7 @@ class NativeSparseAttnBackend(
|
||||
"MLA partial current reuse cp_rank=%s layer=%s current_locs=%s remapped=%s kv_ck=%s rope_ck=%s",
|
||||
forward_batch.cp_shared_kv_layout.cp_rank,
|
||||
layer.layer_id,
|
||||
tensor_debug_summary(forward_batch.out_cache_loc),
|
||||
tensor_debug_summary(current_locs_for_reuse),
|
||||
tensor_debug_summary(page_table_1),
|
||||
tensor_debug_checksum(k),
|
||||
tensor_debug_checksum(k_rope),
|
||||
|
||||
@@ -154,6 +154,17 @@ def prepare_mlp_sync_batch_raw(
|
||||
disable_overlap_schedule: bool,
|
||||
offload_tags: set[str],
|
||||
):
|
||||
skip_all_gather = envs.SGLANG_SCHEDULER_SKIP_ALL_GATHER.get()
|
||||
|
||||
# With a single DP scheduling domain, all TP/CP ranks are expected to have
|
||||
# the same local batch presence. If there is no local batch, an MLP-sync
|
||||
# collective would only gather all-zero idle metadata. More importantly,
|
||||
# in CP disaggregated prefill this idle NCCL/Gloo sync can race with the
|
||||
# next request-broadcast collective after tiny health/internal requests and
|
||||
# leave some ranks in recv broadcast while others enter this all-gather.
|
||||
if local_batch is None and dp_size == 1 and not skip_all_gather:
|
||||
return None
|
||||
|
||||
# Check if other DP workers have running batches
|
||||
if local_batch is None or local_batch.forward_mode.is_prebuilt():
|
||||
num_tokens = 0
|
||||
@@ -176,7 +187,6 @@ def prepare_mlp_sync_batch_raw(
|
||||
or num_tokens_for_logprob == local_batch.batch_size()
|
||||
)
|
||||
|
||||
skip_all_gather = envs.SGLANG_SCHEDULER_SKIP_ALL_GATHER.get()
|
||||
can_cuda_graph = (
|
||||
local_batch is None
|
||||
or local_batch.forward_mode.is_decode_or_idle()
|
||||
|
||||
@@ -51,6 +51,82 @@ class SchedulerOutputProcessorMixin:
|
||||
storage_backend_type = type(storage_backend).__name__
|
||||
return storage_backend_type
|
||||
|
||||
def _maybe_log_eagle_accept_debug(
|
||||
self: Scheduler,
|
||||
batch: ScheduleBatch,
|
||||
result: GenerationBatchResult,
|
||||
) -> None:
|
||||
"""Low-frequency EAGLE accept diagnostics for PD/debug runs.
|
||||
|
||||
This is intentionally gated. The hot path normally only reports an
|
||||
aggregate accept length; when accept length collapses we need enough
|
||||
per-request state to distinguish draft rejection from transfer/cache
|
||||
lifecycle issues without adding per-token or per-layer logging.
|
||||
"""
|
||||
|
||||
if not envs.SGLANG_EAGLE_ACCEPT_DEBUG.get():
|
||||
return
|
||||
if batch.spec_algorithm.is_none():
|
||||
return
|
||||
|
||||
accept_per_req = getattr(result, "accept_length_per_req_cpu", None)
|
||||
if accept_per_req is None:
|
||||
return
|
||||
|
||||
counter = getattr(self, "_eagle_accept_debug_counter", 0) + 1
|
||||
self._eagle_accept_debug_counter = counter
|
||||
|
||||
interval = envs.SGLANG_EAGLE_ACCEPT_DEBUG_INTERVAL.get()
|
||||
interval = max(1, int(interval or 1))
|
||||
has_zero_accept = any(int(x) <= 0 for x in accept_per_req)
|
||||
if not has_zero_accept and counter % interval != 0:
|
||||
return
|
||||
|
||||
spec_info = getattr(batch, "spec_info", None)
|
||||
|
||||
def _shape(obj):
|
||||
shape = getattr(obj, "shape", None)
|
||||
return tuple(shape) if shape is not None else None
|
||||
|
||||
samples = []
|
||||
for i, req in enumerate(batch.reqs[: min(4, len(batch.reqs))]):
|
||||
if not has_zero_accept and counter % interval != 0:
|
||||
break
|
||||
if has_zero_accept and int(accept_per_req[i]) > 0:
|
||||
continue
|
||||
samples.append(
|
||||
{
|
||||
"rid": str(getattr(req, "rid", ""))[:8],
|
||||
"accepted_draft": int(accept_per_req[i]),
|
||||
"origin": len(getattr(req, "origin_input_ids", []) or []),
|
||||
"output": len(getattr(req, "output_ids", []) or []),
|
||||
"cached": int(getattr(req, "cached_tokens", 0) or 0),
|
||||
"spec_verify_ct": int(getattr(req, "spec_verify_ct", 0) or 0),
|
||||
"has_pd_hidden": getattr(req, "hidden_states_tensor", None)
|
||||
is not None,
|
||||
"pd_hidden_shape": _shape(getattr(req, "hidden_states_tensor", None)),
|
||||
"pd_topk_shape": _shape(getattr(req, "output_topk_p", None)),
|
||||
}
|
||||
)
|
||||
|
||||
log_fn = logger.warning if has_zero_accept else logger.info
|
||||
log_fn(
|
||||
"[EAGLE_ACCEPT_DEBUG] step=%d mode=%s bs=%d avg_accept=%.3f "
|
||||
"min_accept=%d max_accept=%d num_zero=%d spec_v2=%s "
|
||||
"spec_topk_shape=%s spec_hidden_shape=%s samples=%s",
|
||||
counter,
|
||||
getattr(self, "disaggregation_mode", None),
|
||||
len(batch.reqs),
|
||||
sum(int(x) for x in accept_per_req) / max(1, len(accept_per_req)),
|
||||
min(int(x) for x in accept_per_req),
|
||||
max(int(x) for x in accept_per_req),
|
||||
sum(1 for x in accept_per_req if int(x) <= 0),
|
||||
getattr(batch, "is_spec_v2", None),
|
||||
_shape(getattr(spec_info, "topk_p", None)),
|
||||
_shape(getattr(spec_info, "hidden_states", None)),
|
||||
samples,
|
||||
)
|
||||
|
||||
def _get_cached_tokens_details(self, req: Req) -> Optional[dict]:
|
||||
"""Get detailed cache breakdown for a request, if available.
|
||||
|
||||
@@ -410,6 +486,7 @@ class SchedulerOutputProcessorMixin:
|
||||
self.num_generated_tokens += len(batch.reqs)
|
||||
if not batch.spec_algorithm.is_none():
|
||||
self.update_spec_metrics(batch.batch_size(), result.num_accepted_tokens)
|
||||
self._maybe_log_eagle_accept_debug(batch, result)
|
||||
if self.enable_metrics:
|
||||
self.metrics_collector.increment_decode_cuda_graph_pass(
|
||||
value=can_run_cuda_graph
|
||||
|
||||
@@ -349,10 +349,11 @@ def _evict_for_compute_owner_lanes(
|
||||
)
|
||||
return
|
||||
|
||||
evict_tokens = max(
|
||||
allocator.page_size,
|
||||
deficit_pages * allocator.page_size * int(getattr(allocator, "cp_size", 1)),
|
||||
)
|
||||
# ``deficits`` is already expressed in physical owner-lane pages. The
|
||||
# old ``* cp_size`` multiplier turned a one-page lane deficit into one
|
||||
# full CP stripe of eviction and caused large L1 churn under HiCache
|
||||
# load-back pressure.
|
||||
evict_tokens = max(allocator.page_size, deficit_pages * allocator.page_size)
|
||||
before_available = allocator.available_size()
|
||||
logger.info(
|
||||
"[MemCache-evict] _evict_for_compute_owner_lanes attempt=%d: deficit_pages=%d evict_tokens=%d before_available=%d evictable_size=%d",
|
||||
|
||||
@@ -1238,6 +1238,105 @@ class HiRadixCache(RadixCache):
|
||||
)
|
||||
return refreshed
|
||||
|
||||
def _evict_cp_owner_lane_deficit_nodes(
|
||||
self, params: EvictParams
|
||||
) -> EvictResult:
|
||||
deficits = [max(0, int(v)) for v in (params.owner_lane_deficits or [])]
|
||||
if not deficits or all(v <= 0 for v in deficits):
|
||||
return EvictResult()
|
||||
|
||||
plan = CpLoadBackPlan(
|
||||
page_owners=[],
|
||||
required_by_owner=[],
|
||||
available_by_owner=[],
|
||||
deficit_by_owner=deficits,
|
||||
host_hit_len=0,
|
||||
)
|
||||
eviction_plan = self._plan_cp_load_back_owner_lane_evictions(plan)
|
||||
if len(eviction_plan.victims) == 0:
|
||||
logger.info(
|
||||
"[HiCache-evict] owner-lane evict found no contributing victims: "
|
||||
"num_tokens=%d deficits=%s evictable_size=%d available_size=%d",
|
||||
params.num_tokens,
|
||||
deficits,
|
||||
self.evictable_size_,
|
||||
self.token_to_kv_pool_allocator.available_size(),
|
||||
)
|
||||
return EvictResult()
|
||||
|
||||
num_evicted = 0
|
||||
num_locked_skipped = 0
|
||||
write_back_nodes: List[TreeNode] = []
|
||||
for victim in eviction_plan.victims:
|
||||
if victim.lock_ref > 0:
|
||||
num_locked_skipped += 1
|
||||
continue
|
||||
|
||||
if victim.pin_expiry > 0 and time.monotonic() > victim.pin_expiry:
|
||||
self._clear_pin(victim)
|
||||
|
||||
if not self._cp_device_leaf_is_load_back_victim(victim):
|
||||
logger.info(
|
||||
"[HiCache-evict] owner-lane evict victim no longer evictable: "
|
||||
"victim_id=%s deficits=%s",
|
||||
getattr(victim, "id", None),
|
||||
deficits,
|
||||
)
|
||||
continue
|
||||
|
||||
if self._is_pinned(victim):
|
||||
if self._node_backuped(victim):
|
||||
num_evicted += self._evict_backuped(victim)
|
||||
else:
|
||||
written = self.write_backup(victim, write_back=True)
|
||||
if written > 0:
|
||||
write_back_nodes.append(victim)
|
||||
else:
|
||||
self._clear_pin(victim)
|
||||
continue
|
||||
|
||||
if not self._node_backuped(victim):
|
||||
if self.cache_controller.write_policy == "write_back":
|
||||
written = self.write_backup(victim, write_back=True)
|
||||
if written > 0:
|
||||
write_back_nodes.append(victim)
|
||||
continue
|
||||
num_evicted += self._evict_regular(victim)
|
||||
else:
|
||||
num_evicted += self._evict_backuped(victim)
|
||||
|
||||
for child in victim.parent.children.values():
|
||||
if child in write_back_nodes:
|
||||
continue
|
||||
if not child.evicted:
|
||||
break
|
||||
else:
|
||||
self._update_leaf_status(victim.parent)
|
||||
|
||||
if write_back_nodes:
|
||||
self.writing_check(write_back=True)
|
||||
for victim in write_back_nodes:
|
||||
if self._node_backuped(victim):
|
||||
num_evicted += self._evict_backuped(victim)
|
||||
|
||||
logger.info(
|
||||
"[HiCache-evict] owner-lane evict END: requested_tokens=%d "
|
||||
"deficits=%s victims=%s planned_freed_by_owner=%s "
|
||||
"remaining_deficit_by_owner=%s num_evicted=%d "
|
||||
"num_locked_skipped=%d evictable_size_after=%d "
|
||||
"available_size_after=%d",
|
||||
params.num_tokens,
|
||||
deficits,
|
||||
[getattr(node, "id", None) for node in eviction_plan.victims],
|
||||
eviction_plan.planned_freed_by_owner,
|
||||
eviction_plan.remaining_deficit_by_owner,
|
||||
num_evicted,
|
||||
num_locked_skipped,
|
||||
self.evictable_size_,
|
||||
self.token_to_kv_pool_allocator.available_size(),
|
||||
)
|
||||
return EvictResult(num_tokens_evicted=num_evicted)
|
||||
|
||||
def _cp_build_write_admission(
|
||||
self, device_indices: torch.Tensor, *, node_id: int, phase: str
|
||||
) -> CpWriteAdmission:
|
||||
@@ -2575,6 +2674,13 @@ class HiRadixCache(RadixCache):
|
||||
def evict(self, params: EvictParams) -> EvictResult:
|
||||
start_time = time.perf_counter()
|
||||
num_tokens = params.num_tokens
|
||||
if params.owner_lane_deficits is not None and any(
|
||||
int(v) > 0 for v in params.owner_lane_deficits
|
||||
):
|
||||
result = self._evict_cp_owner_lane_deficit_nodes(params)
|
||||
self.update_eviction_metrics(result.num_tokens_evicted, start_time)
|
||||
return result
|
||||
|
||||
leaves = list(self.evictable_leaves)
|
||||
eviction_heap = [
|
||||
(self.eviction_strategy.get_priority(node), node) for node in leaves
|
||||
|
||||
@@ -494,6 +494,28 @@ class RadixCache(BasePrefixCache):
|
||||
end = start
|
||||
self.token_to_kv_pool_allocator.free(kv_indices[start:end])
|
||||
|
||||
def _should_free_cp_exact_match_tail(
|
||||
self, *, start: int, end: int, key_len: int
|
||||
) -> bool:
|
||||
"""Return whether duplicate-prefix freeing must include a CP tail page.
|
||||
|
||||
CP HiCache keeps radix keys at scheduler-visible valid lengths while the
|
||||
allocator owns whole physical pages. For an exact duplicate hit whose
|
||||
valid key ends inside a page (typical tiny EAGLE/bigram requests), the
|
||||
duplicate request page is not retained by a new radix node. Flooring
|
||||
``end`` to the previous page boundary would therefore leak that newly
|
||||
allocated page. Partial/new-node insertions still use the conservative
|
||||
page-floored free path because their tail page is represented by the
|
||||
radix node being inserted.
|
||||
"""
|
||||
|
||||
return (
|
||||
getattr(self, "_uses_cp_hicache", False)
|
||||
and key_len > 0
|
||||
and end == key_len
|
||||
and end > start
|
||||
)
|
||||
|
||||
def cache_finished_req(self, req: Req, is_insert: bool = True):
|
||||
"""Cache request when it finishes."""
|
||||
# In deterministic mode, disable finished request insertion to radix cache
|
||||
@@ -541,7 +563,14 @@ class RadixCache(BasePrefixCache):
|
||||
new_prefix_len = result.prefix_len
|
||||
# Free the duplicates that were already in the tree
|
||||
self._free_kv_indices_range(
|
||||
kv_indices, req.cache_protected_len, new_prefix_len
|
||||
kv_indices,
|
||||
req.cache_protected_len,
|
||||
new_prefix_len,
|
||||
include_partial_tail=self._should_free_cp_exact_match_tail(
|
||||
start=req.cache_protected_len,
|
||||
end=new_prefix_len,
|
||||
key_len=len(keys),
|
||||
),
|
||||
)
|
||||
else:
|
||||
if prepared_cp_backup is not None and hasattr(
|
||||
@@ -612,7 +641,16 @@ class RadixCache(BasePrefixCache):
|
||||
req.cp_hicache_prepared_backup = None
|
||||
new_prefix_len = result.prefix_len
|
||||
|
||||
self._free_kv_indices_range(kv_indices, req.cache_protected_len, new_prefix_len)
|
||||
self._free_kv_indices_range(
|
||||
kv_indices,
|
||||
req.cache_protected_len,
|
||||
new_prefix_len,
|
||||
include_partial_tail=self._should_free_cp_exact_match_tail(
|
||||
start=req.cache_protected_len,
|
||||
end=new_prefix_len,
|
||||
key_len=len(keys),
|
||||
),
|
||||
)
|
||||
|
||||
# The prefix indices could be updated, reuse it
|
||||
match_result = self.match_prefix(MatchPrefixParams(key=radix_key))
|
||||
|
||||
Reference in New Issue
Block a user