L2 prereqs for CP8DP2 + L3: scope HiCache consensus to the CP group + reset pooled allocator on flush

Two latent CP shared-KV L2 correctness fixes, landed BEFORE L3 (not in an L3
commit). Surfaced by a per-CP8-instance (CP8DP2EP16) scoping review.

PREREQ-1 (CP-group scoping). The B1 commit/evict consensus collectives
(writing_check ReduceOp.MIN, placement_digest MIN/MAX + its tp_world_size<=1
entry guard, drain_storage_control_queues, the evict/prefetch MINs, the flush
barrier) AND cache_controller.prefetch_tp_group all derive from
self.tp_group/self.tp_world_size, which was params.tp_cache_group. tp_cache_group
equals the CP group ONLY at dp_size=1 (enable_dp_attention False -> tp_cpu_group,
and attn_cp_size==tp_size -> _ATTN_CP==_TP) -- the sole reason B1 works today.
Under CP8DP2 (DP attention, attn_tp_size=1) tp_cache_group is the size-1 attn-TP
group, so every `tp_world_size>1` collective silently no-ops per rank and the
placement assert self-disables -> divergent placement -> shared-slab corruption.
Fix: for CP hicache (cp_size>1) scope self.tp_group to the CP cpu group
(get_attention_cp_group().cpu_group -- already used for the slab-handle
broadcast) + self.tp_world_size to its size. A no-op handle change at dp_size=1
(same group object); the intended fix at CP8DP2. The single init-point change
propagates to every consensus collective + un-gates prefetch_tp_group + re-enables
the placement assert.

PREREQ-2 (flush reset). HiRadixCache.reset() cleared the radix tree + host pool
but never reset CpSharedL2PageAllocator -> stale free list/ranges/committed after
flush_cache (leak; the shared pool was never reclaimed). Added
CpSharedL2PageAllocator.reset() (rebuild the per-slab free list all-free, drop
ranges + committed, restore the freshly-built placement_digest) called from
cache_controller.reset() after the ack queues are cleared. Safe: flush_cache is
idle-gated (no in-flight backup/reserve). This is also L3's clear hookpoint.

Validation: new test_reset_restores_freshly_constructed_all_free_state + 89/89
pool suite (torch-2.11 container) + import smoke. PREREQ-1 is a no-op at dp_size=1
(live no-regression confirmed on the next prefill restart); CP8DP2 correctness is
by construction (CP-group membership verified) pending a 2-machine run.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-20 21:24:54 +00:00
parent 9549b268d7
commit 9368c33b23
4 changed files with 70 additions and 1 deletions

View File

@@ -867,6 +867,14 @@ class HiCacheController:
self.prefetch_thread.start()
self.backup_thread.start()
# PREREQ-2: reset the B1 pooled-L2 allocator so flush_cache reclaims the
# shared pool. The radix tree + host pool are cleared by the caller; without
# this the allocator free list / ranges / committed set stay stale (leak).
# Safe here: the ack queues are cleared above and flush_cache is idle-gated
# (no in-flight backup/reserve referencing dropped ranges).
if self.cp_shared_l2_page_allocator is not None:
self.cp_shared_l2_page_allocator.reset()
def clear_draft_host_pool(self) -> None:
if self.draft_mem_pool_host is not None:
self.draft_mem_pool_host.clear()

View File

@@ -607,6 +607,26 @@ class CpSharedL2PageAllocator:
"cp_shared_l2_objects_evicted": 0,
}
def reset(self) -> None:
"""Reset to the freshly-constructed all-free state (flush_cache hook).
Rebuilds the per-slab free list to fully-free and drops all object ranges +
the committed set, so flush_cache reclaims the pooled L2 (the allocator
metadata; the slab bytes are left as-is -- unreferenced once the radix tree
and ranges are cleared). Rank-uniform: every CP rank runs it at the
idle-gated flush point, so the post-reset placement_digest is empty and
identical on every rank. Cumulative stats are preserved.
"""
self._generation = 0
self._ranges_by_object = {}
self._committed_objects = set()
self._free_by_payload = {
payload_kind: {
slab.slab_id: [(0, slab.num_pages)] for slab in slabs
}
for payload_kind, slabs in self._slabs_by_payload.items()
}
def reserve(
self, object_key: str, payload_kind: str, num_pages: int
) -> CpSharedL2ObjectRange:

View File

@@ -1015,7 +1015,26 @@ class HiRadixCache(RadixCache):
slabs_by_payload=cp_shared_l2_slabs_by_payload,
)
self.tp_group = params.tp_cache_group
# PREREQ-1 (CP8DP2 scoping): the B1 commit/evict consensus collectives
# (writing_check MIN, placement_digest, drain/evict MINs, the prefetch group,
# the flush barrier) MUST reduce over the CP group -- the ranks that share the
# replicated event stream + the MAP_SHARED slab. tp_cache_group equals the CP
# group ONLY at dp_size=1 (CP==TP); under CP8DP2 (DP attention) tp_cache_group
# is the size-1 attn-TP group, so every `tp_world_size > 1` collective would
# silently no-op per rank -> divergent placement -> shared-slab corruption.
# Scope to the CP cpu group (a no-op handle change at dp_size=1).
_cp_hicache_cp_size = (
int(getattr(params.token_to_kv_pool_allocator, "cp_size", 1) or 1)
if self._uses_cp_hicache
else 1
)
if self._uses_cp_hicache and _cp_hicache_cp_size > 1:
from sglang.srt.layers.dp_attention import get_attention_cp_group
_cp_group = get_attention_cp_group()
self.tp_group = getattr(_cp_group, "cpu_group", _cp_group)
else:
self.tp_group = params.tp_cache_group
self.tp_world_size = torch.distributed.get_world_size(group=self.tp_group)
self.pp_rank = params.pp_rank
self.pp_size = params.pp_size