CP HiCache: redesign owner-lane eviction planner as a lazy-re-evaluation heap (O(n log n))
Replaces the O(victims*candidates) per-iteration greedy argmin rescan in _plan_cp_load_back_owner_lane_evictions with a leaf-up min-heap. The score (-contribution, -unlock, slru_priority, node.id) depends on the current deficits, so a static heap is not equivalent; instead use LAZY RE-EVALUATION: a popped entry is consumed only if its score still matches the node's current score (deficits unchanged since push), else it is re-pushed with the fresh score. Stale scores are always optimistic (contribution = sum(min(counts[o], deficits[o])) and the ancestor-unlock contribution only shrink as deficits shrink), so the first up-to-date popped entry is exactly the global argmin the full rescan would have picked -> PROVABLY EQUIVALENT, with the same leaf-up eligibility + ancestor-unlock + parent-push. Determinism/rank-uniformity preserved (selection decided by the total-ordered score; the heap insertion seq only orders structurally-equal tuples). Equivalence proven by test: a verbatim reference greedy + a randomized property test (300 seeds, single- AND multi-owner deficits, non-uniform counts exercising the lazy-re-eval boundary, varied SLRU priorities) asserting byte-identical (victims, planned_freed, remaining), plus an explicit leaf-up + ancestor-unlock case (child-then-parent). 14 planner tests pass (1 pre-existing unrelated EAGLE-tail failure unchanged). Micro-bench (V100, candidates=2000 deficit=7877, benchmark/hicache/bench_cp_owner_lane_planner.py): A. ORIGINAL (.item() x cp, no memo) 50.06s B. memo + bincount (greedy) 0.494s (101x) C. lazy-re-eval heap 0.162s (309x; 3.0x over B) selection A==B==C identical Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -88,29 +88,54 @@ def plan_greedy(counts_fn, use_memo):
|
||||
|
||||
|
||||
def plan_heap(counts_fn):
|
||||
"""Redesign (single-owner deficit): compute counts once, sort by -counts[owner], one pass.
|
||||
Byte-identical victim SET to greedy for a single-owner deficit (highest owner-count first,
|
||||
id tiebreak) -- the contribution is min(counts[owner], remaining) == counts[owner] until the
|
||||
final pick."""
|
||||
owner = DEFICIT_OWNER
|
||||
scored = []
|
||||
for nid, value in NODES:
|
||||
counts = counts_fn(value)
|
||||
if counts[owner] <= 0:
|
||||
continue
|
||||
scored.append((counts, nid))
|
||||
scored.sort(key=lambda x: (-x[0][owner], x[1]))
|
||||
rem = [0] * CP
|
||||
rem[owner] = DEFICIT
|
||||
"""The SHIPPED heap redesign: lazy-re-evaluation min-heap (a popped entry is used only if
|
||||
its score is still current, else re-pushed). Provably equivalent to the greedy argmin
|
||||
rescan; here flat (no tree) so it isolates the algorithmic cost vs B."""
|
||||
import heapq
|
||||
|
||||
deficits = [0] * CP
|
||||
deficits[DEFICIT_OWNER] = DEFICIT
|
||||
planned_evicted = set()
|
||||
planned_freed = [0] * CP
|
||||
memo = {}
|
||||
heap = []
|
||||
seq = 0
|
||||
|
||||
def scored(nid, value):
|
||||
if nid in memo:
|
||||
counts = memo[nid]
|
||||
else:
|
||||
counts = counts_fn(value)
|
||||
memo[nid] = counts
|
||||
contribution = sum(min(c, d) for c, d in zip(counts, deficits))
|
||||
if contribution <= 0:
|
||||
return None
|
||||
return (-contribution, nid), counts
|
||||
|
||||
for nid, value in NODES:
|
||||
s = scored(nid, value)
|
||||
if s is not None:
|
||||
heapq.heappush(heap, (s[0], seq, nid, s[1]))
|
||||
seq += 1
|
||||
|
||||
victims = 0
|
||||
for counts, nid in scored:
|
||||
if all(v <= 0 for v in rem):
|
||||
break
|
||||
while any(v > 0 for v in deficits) and heap:
|
||||
score, _s, nid, counts = heapq.heappop(heap)
|
||||
if nid in planned_evicted:
|
||||
continue
|
||||
fresh = scored(nid, None) # counts already memoized
|
||||
if fresh is None:
|
||||
continue
|
||||
if fresh[0] != score:
|
||||
heapq.heappush(heap, (fresh[0], seq, nid, fresh[1]))
|
||||
seq += 1
|
||||
continue
|
||||
counts = fresh[1]
|
||||
planned_evicted.add(nid)
|
||||
victims += 1
|
||||
for o, c in enumerate(counts):
|
||||
planned_freed[o] += c
|
||||
rem[o] = max(0, rem[o] - c)
|
||||
victims += 1
|
||||
deficits[o] = max(0, deficits[o] - c)
|
||||
return victims, tuple(planned_freed)
|
||||
|
||||
|
||||
@@ -133,7 +158,7 @@ def main():
|
||||
|
||||
vO, fO = timed("A. ORIGINAL (.item() x cp, no memo)", lambda: plan_greedy(counts_original, False))
|
||||
vF, fF = timed("B. FIXED (bincount + per-plan memo)", lambda: plan_greedy(counts_fixed, True))
|
||||
vH, fH = timed("C. REDESIGN (counts-once + sorted pass)", lambda: plan_heap(counts_fixed))
|
||||
vH, fH = timed("C. REDESIGN (lazy-re-eval heap)", lambda: plan_heap(counts_fixed))
|
||||
|
||||
print("\nselection identical (victims, planned_freed):")
|
||||
print(f" A==B: {(vO, fO) == (vF, fF)} A==C: {(vO, fO) == (vH, fH)}")
|
||||
|
||||
Reference in New Issue
Block a user