L3 3.3: continuous-background, communication-free, page-level LRU GC (+ wire the reload touch)

Replaces §9.3's object-level rank-uniform eviction with per-rank, page-level LRU GC running ON THE
WRITE THREAD (the sole slot-pool owner -> no lock, no second owner). Genuinely communication-free:
each (hash,payload) is written/read/deleted by exactly its one owner (owner = i%cp_size, and the
prefix-chained content hash makes one-hash = one-position = one-owner), so the shared LMDB's own
consistency substitutes for collectives within L3 (design §9.5/§9.6). GC touches only L3, adds zero
collectives; the spill/reload MINs that remain are all the L2 bridge.

cp_l3_store.py:
- Per-payload lazy-deletion min-heap `_gc_heap` of (last_access, seq, hash) + authoritative
  `_gc_current{hash->(last_access,slot)}`. Add on write, touch via `_touch_q`, reclaim coldest.
- The write loop, each iteration: drain touches -> _gc_collect (reclaim coldest of any pool over the
  0.90 start watermark down to 0.85, bounded budget) BEFORE the next write, so spill never hits a full
  pool (no evict burst on the critical path). Continuous + proactive, symmetric with the spill.
- Reclaim deletes the index entry (durable) BEFORE freeing the slot, so a racing reload reads None or a
  CRC-mismatched reused slot -> miss -> recompute (fail-soft); no lock / in-flight-exclusion needed.
- submit_touch (scheduler->write thread); clear() resets the GC structures.

hiradix_cache.py: wire the reload `touch` (the 3.2 gap -- hit_count/last_access never updated): at
_cp_l3_admit_reload, bump the L3 last_access of THIS rank's owned reloaded pages (ALL pages, not just
the tail, so a prefix ages uniformly). _cp_l3_insert_reloaded_node now returns the node (its
last_access seeds the touch).

Sharing handled optimally for free (page = one entry, last_access = max over touchers; prefix pages
age together; shared-hot survives) -- no object-record table, no refcount, no content-keying. 56 L3
unit tests green (new: GC reclaims coldest-to-watermark + no slot leak; touch keeps a page warm).
Cold-rebuild (scan slab headers + LMDB) is designed (§9.6) but deferred to 3.4 (crash-recovery).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-23 01:39:47 +00:00
co-authored by Claude Opus 4.8
parent 864b1c808e
commit e96cfa6cbd
3 changed files with 184 additions and 13 deletions
@@ -93,12 +93,13 @@ class TestCpL3Store(unittest.TestCase):
self.store.close()
self._td.cleanup()
def _finish_spill(self):
def _finish_spill(self, store=None):
"""Drive the 2-phase spill to completion (store-level; no cross-rank MIN). Returns durable acks
as [(op_id, ok), ...]. Draining the gather-ack is what releases the eviction pin in production."""
self.assertTrue(_wait_ack(self.store.ack_durable_qsize, 1))
self.store.drain_gather_acks(self.store.ack_gather_qsize())
return self.store.drain_durable_acks(self.store.ack_durable_qsize())
st = store if store is not None else self.store
self.assertTrue(_wait_ack(st.ack_durable_qsize, 1))
st.drain_gather_acks(st.ack_gather_qsize())
return st.drain_durable_acks(st.ack_durable_qsize())
def test_two_phase_gather_precedes_durable(self):
# the core 3.1 fix: gather (pin release) acks BEFORE the slow durable write; inflight until durable.
@@ -200,6 +201,77 @@ class TestCpL3Store(unittest.TestCase):
self.assertEqual(pool.num_free, 1) # no orphan: the partial alloc was rolled back
self.assertFalse(store.has_inflight())
def test_gc_reclaims_coldest_to_stop_watermark(self):
# 3.3 continuous-background GC: filling a pool past the start watermark triggers the write-thread GC to
# reclaim the COLDEST (lowest last_access) owned pages down to the stop watermark, deleting their index
# entries, with no slot leak. Small pool so the watermark trips deterministically.
td = tempfile.TemporaryDirectory()
self.addCleanup(td.cleanup)
cfg = cfg_mod.CpL3Config.from_dict({
"backend": "posix", "require_plp": False, "index_map_gb": 0.05,
"disks": [{"path": os.path.join(td.name, "d"), "budget_gb": 0.0001}],
})
store = store_mod.CpL3Store.from_config(
cfg, cp_rank=0, cp_size=1, accessors={"target_kv": self.acc})
store.connect(cfg)
self.addCleanup(store.close)
pool = store.pools["target_kv"]
n = pool.num_slots
self.assertGreaterEqual(n, 8)
start = int(store._gc_start_frac * n)
stop = int(store._gc_stop_frac * n)
# spill n distinct pages, page i with last_access=i (so smaller i == colder)
for i in range(n):
store.submit_spill(f"o{i}", {"target_kv": [(i % PAGE_NUM, _h(3000 + i))]}, last_access=i)
self._finish_spill(store)
# the write-thread GC keeps usage within the watermark band [stop, start] (reclaim-to-stop on trigger)
t0 = time.time()
while pool.num_allocated > start and time.time() - t0 < 5.0:
time.sleep(0.01)
allocated = pool.num_allocated
self.assertLessEqual(allocated, start)
self.assertGreaterEqual(allocated, stop)
self.assertEqual(pool.num_free + allocated, n) # no slot leak
n_evicted = n - allocated
self.assertGreater(n_evicted, 0)
for i in range(n_evicted): # the coldest n_evicted pages were reclaimed (index entries deleted)
self.assertEqual(store.exists_prefix([_h(3000 + i)], ["target_kv"]), 0)
for i in range(n_evicted, n): # the warmest survive
self.assertEqual(store.exists_prefix([_h(3000 + i)], ["target_kv"]), 1)
def test_gc_touch_keeps_a_page_warm(self):
# submit_touch bumps a page's last_access so it survives GC while colder untouched pages are reclaimed.
td = tempfile.TemporaryDirectory()
self.addCleanup(td.cleanup)
cfg = cfg_mod.CpL3Config.from_dict({
"backend": "posix", "require_plp": False, "index_map_gb": 0.05,
"disks": [{"path": os.path.join(td.name, "d"), "budget_gb": 0.0001}],
})
store = store_mod.CpL3Store.from_config(
cfg, cp_rank=0, cp_size=1, accessors={"target_kv": self.acc})
store.connect(cfg)
self.addCleanup(store.close)
pool = store.pools["target_kv"]
n = pool.num_slots
start = int(store._gc_start_frac * n)
stop = int(store._gc_stop_frac * n)
self.assertGreaterEqual(start, 4)
# fill below the start watermark (no GC yet), page i last_access=i
for i in range(start):
store.submit_spill(f"o{i}", {"target_kv": [(i % PAGE_NUM, _h(4000 + i))]}, last_access=i)
self._finish_spill(store)
# touch the coldest page (i=0) to the warmest -> it must survive the upcoming GC
store.submit_touch([(_h(4000 + 0), "target_kv", 10_000)])
# now push over the start watermark -> GC reclaims coldest down to stop
for i in range(start, n + 1):
store.submit_spill(f"o{i}", {"target_kv": [(i % PAGE_NUM, _h(4000 + i))]}, last_access=i)
self._finish_spill(store)
t0 = time.time()
while pool.num_allocated > start and time.time() - t0 < 5.0:
time.sleep(0.01)
self.assertEqual(store.exists_prefix([_h(4000 + 0)], ["target_kv"]), 1) # touched -> warm -> survived
self.assertEqual(store.exists_prefix([_h(4000 + 1)], ["target_kv"]), 0) # next-coldest -> reclaimed
def test_clear_resets(self):
self.store.submit_spill("o", {"target_kv": [(0, _h(0))]}, last_access=1)
self._finish_spill()