Batch CP HiCache host admission before write reservation

CP shared-KV prefill batching can present multiple requests to HiCache write-through in one forward. Keeping host free-room admission per request causes repeated small L2 evictions and defeats the free-room policy, while request metadata and radix attachment still need per-request ownership.\n\nThis change adds a batch prepare path that aggregates required host tokens by CP owner lane, performs one host admission/eviction step for the batch, and then reserves/submits each request independently. SessionAwareCache and the scheduler prefer the batch API when available.\n\nConstraint: Radix nodes, host slots, draft slots, rollback, and ack semantics remain per request.\nRejected: Merge reservations or radix nodes across requests | would complicate rollback and writing_check ack ownership.\nRejected: Add collective synchronization for host admission | local owner-lane logic already provides deterministic capacity accounting.\nConfidence: medium\nScope-risk: moderate\nDirective: Do not reintroduce per-request host free-room eviction in the scheduler path without profiling host-full workloads.\nTested: local py_compile for touched Python files\nTested: remote g0034 docker PYTHONPATH=python python -m pytest -q test/registered/unit/mem_cache/test_cp_hicache_metadata.py => 117 passed, 5 warnings\nNot-tested: full ETE bs>1 HiCache run under production traffic
This commit is contained in:
laoyao0822
2026-06-04 04:32:40 +08:00
parent 108e7d866d
commit 02af370e87
5 changed files with 342 additions and 27 deletions

View File

@@ -2654,6 +2654,18 @@ class Scheduler(
):
return
prepare_batch_fn = getattr(
self.tree_cache, "prepare_write_backups_for_reqs", None
)
if prepare_batch_fn is None:
inner_cache = getattr(self.tree_cache, "inner", None)
prepare_batch_fn = getattr(
inner_cache, "prepare_write_backups_for_reqs", None
)
if prepare_batch_fn is not None:
prepare_batch_fn(batch.reqs)
return
prepare_fn = getattr(self.tree_cache, "prepare_write_backup_for_req", None)
if prepare_fn is None:
inner_cache = getattr(self.tree_cache, "inner", None)

View File

@@ -313,6 +313,13 @@ class PreparedCpHiCacheBackup:
attached: bool = False
@dataclass(frozen=True)
class CpWriteBackupCandidate:
req: object
kv_indices: torch.Tensor
node_id: int
@dataclass(frozen=True)
class CpHiCacheCapacitySnapshot:
target_capacity: Tuple[int, ...]
@@ -1407,10 +1414,10 @@ class HiRadixCache(RadixCache):
)
return EvictResult(num_tokens_evicted=num_evicted)
def _cp_build_write_admission(
self, device_indices: torch.Tensor, *, node_id: int, phase: str
def _cp_build_write_admission_from_required(
self, required: Tuple[int, ...], *, node_id: int, phase: str
) -> CpWriteAdmission:
required = self._cp_required_host_tokens_by_rank(device_indices)
required = tuple(int(v) for v in required)
snapshot = self._cp_host_capacity_snapshot()
target_available = self._cp_host_available_tokens_by_rank(snapshot)
draft_available = self._cp_host_available_tokens_by_rank(snapshot, draft=True)
@@ -1418,12 +1425,13 @@ class HiRadixCache(RadixCache):
trigger_ratio = float(
getattr(self, "hicache_host_free_room_trigger_ratio", 0.0) or 0.0
)
page_size = int(getattr(self, "page_size", 1) or 1)
target_deficit = tuple(
_free_room_deficit(
required=req,
available=avail,
capacity=capacity,
page_size=self.page_size,
page_size=page_size,
target_ratio=target_ratio,
trigger_ratio=trigger_ratio,
)
@@ -1438,7 +1446,7 @@ class HiRadixCache(RadixCache):
required=req,
available=avail,
capacity=capacity,
page_size=self.page_size,
page_size=page_size,
target_ratio=target_ratio,
trigger_ratio=trigger_ratio,
)
@@ -1463,6 +1471,16 @@ class HiRadixCache(RadixCache):
eviction_plan=eviction_plan,
)
def _cp_build_write_admission(
self, device_indices: torch.Tensor, *, node_id: int, phase: str
) -> CpWriteAdmission:
required = self._cp_required_host_tokens_by_rank(device_indices)
return self._cp_build_write_admission_from_required(
required,
node_id=node_id,
phase=phase,
)
def shutdown(self):
"""Best-effort auto-detach of storage backend on process shutdown.
@@ -2038,18 +2056,25 @@ class HiRadixCache(RadixCache):
return True
def _reserve_write_cp_indices_no_collective(
self, device_indices: torch.Tensor, node_id: int
self,
device_indices: torch.Tensor,
node_id: int,
*,
admission_checked: bool = False,
):
admission = self._cp_build_write_admission(
device_indices, node_id=node_id, phase="initial"
)
if any(v > 0 for v in admission.deficit_by_owner):
if not self._evict_cp_host_for_write_admission(
admission, node_id=node_id, phase="initial"
):
return HiCacheWriteFailure(
required_host_slots=max(admission.eviction_plan.remaining_deficit)
)
if not admission_checked:
admission = self._cp_build_write_admission(
device_indices, node_id=node_id, phase="initial"
)
if any(v > 0 for v in admission.deficit_by_owner):
if not self._evict_cp_host_for_write_admission(
admission, node_id=node_id, phase="initial"
):
return HiCacheWriteFailure(
required_host_slots=max(
admission.eviction_plan.remaining_deficit
)
)
result = self.cache_controller.reserve_write_cp(
device_indices=device_indices,
@@ -2416,16 +2441,9 @@ class HiRadixCache(RadixCache):
if hasattr(self, "evictable_host_leaves"):
self._update_host_leaf_status(parent)
def prepare_write_backup_for_req(self, req) -> None:
if self.disable or not self._uses_cp_hicache:
return
if self.cache_controller.write_policy == "write_back":
self._warn_cp_hicache_fallback(
"prepare_write_backup_skipped",
"write_back_policy",
rid=getattr(req, "rid", "<unknown>"),
)
return
def _build_prepare_write_backup_candidate_for_req(
self, req
) -> Optional[CpWriteBackupCandidate]:
if getattr(req, "cp_hicache_prepared_backup", None) is not None:
prepared = getattr(req, "cp_hicache_prepared_backup", None)
self._warn_cp_hicache_fallback(
@@ -2485,7 +2503,32 @@ class HiRadixCache(RadixCache):
node_id = TreeNode.counter
TreeNode.counter += 1
result = self._reserve_write_cp_indices_no_collective(kv_indices, node_id)
return CpWriteBackupCandidate(
req=req,
kv_indices=kv_indices,
node_id=node_id,
)
def _submit_prepare_write_backup_candidate(
self,
candidate: CpWriteBackupCandidate,
*,
admission_checked: bool = False,
) -> None:
req = candidate.req
kv_indices = candidate.kv_indices
node_id = candidate.node_id
if admission_checked:
result = self._reserve_write_cp_indices_no_collective(
kv_indices,
node_id,
admission_checked=True,
)
else:
result = self._reserve_write_cp_indices_no_collective(
kv_indices,
node_id,
)
if isinstance(result, HiCacheWriteFailure):
self._warn_cp_hicache_fallback(
"prepare_write_backup_reservation_failed",
@@ -2519,6 +2562,81 @@ class HiRadixCache(RadixCache):
result.metadata.owned_positions.numel(),
)
def prepare_write_backup_for_req(self, req) -> None:
if self.disable or not self._uses_cp_hicache:
return
if self.cache_controller.write_policy == "write_back":
self._warn_cp_hicache_fallback(
"prepare_write_backup_skipped",
"write_back_policy",
rid=getattr(req, "rid", "<unknown>"),
)
return
candidate = self._build_prepare_write_backup_candidate_for_req(req)
if candidate is None:
return
self._submit_prepare_write_backup_candidate(candidate)
def prepare_write_backups_for_reqs(self, reqs) -> None:
if self.disable or not self._uses_cp_hicache:
return
reqs = list(reqs)
if len(reqs) == 0:
return
if self.cache_controller.write_policy == "write_back":
self._warn_cp_hicache_fallback(
"prepare_write_backups_skipped",
"write_back_policy",
batch_size=len(reqs),
)
return
candidates: List[CpWriteBackupCandidate] = []
for req in reqs:
candidate = self._build_prepare_write_backup_candidate_for_req(req)
if candidate is not None:
candidates.append(candidate)
if len(candidates) == 0:
return
admission_checked = False
if len(candidates) > 1:
required = self._cp_zero_counts()
for candidate in candidates:
required = self._cp_add_counts(
required,
self._cp_required_host_tokens_by_rank(candidate.kv_indices),
)
admission = self._cp_build_write_admission_from_required(
required,
node_id=candidates[0].node_id,
phase="batch_prepare",
)
if any(v > 0 for v in admission.deficit_by_owner):
if self._evict_cp_host_for_write_admission(
admission,
node_id=candidates[0].node_id,
phase="batch_prepare",
):
admission_checked = True
else:
self._warn_cp_hicache_fallback(
"prepare_write_backups_batch_admission_failed",
"host_reservation_failed",
batch_size=len(candidates),
required_by_owner=required,
remaining_deficit=admission.eviction_plan.remaining_deficit,
)
else:
admission_checked = True
for candidate in candidates:
self._submit_prepare_write_backup_candidate(
candidate,
admission_checked=admission_checked,
)
def _node_host_len(self, node: TreeNode) -> int:
if self._uses_cp_hicache:
return node.host_len

View File

@@ -337,6 +337,14 @@ class SessionAwareCache(BasePrefixCache):
return prepare_fn(req)
return None
def prepare_write_backups_for_reqs(self, reqs) -> None:
prepare_fn = getattr(self.inner, "prepare_write_backups_for_reqs", None)
if prepare_fn is not None:
return prepare_fn(reqs)
for req in reqs:
self.prepare_write_backup_for_req(req)
return None
def check_hicache_events(self):
return self.inner.check_hicache_events()