Repository navigation
Selene host-tier eviction: unified host memory, local spill on pressure, optional max age - #141
Merged
Merged
Conversation
Keep the upstream quarter split for row and page slabs. Remove concurrency from slab sizing and preparation; the existing row warning alone consumes selected concurrency and fires when allocated rows fall below it. Floor MemoryStore's page headroom at its largest record, sharing the accessor between composite spills and memory-only pool retirement. Poll memory-only stores through the existing retire_for path before another publication needs the slabs. Cover both paths with CPU large-record regressions. Keep GPU retry records unpinned and document early spills for near-capacity records. Production net: +8 lines, excluding tests; all-file net: +92 lines. Validation: CI-exact locked release no-run builds for Selene and manager with test-support, Helios server/runtime-package lib and Flare; both RDMA checks; and the Selene and manager suites pass. The manager suite was rerun with 4 GiB of container shared memory after exhausting the default 64 MiB. One client fixture failed to start its manager and passed on an isolated retry. GPU serving and performance comparisons were not run. Design-Signed-Off-By: yatesdr; design-review=1c01
Local and pooled composite stores park memory copies and use the existing headroom spill path. Shutdown uses the existing opt-in drain once and preserves committed files. Default stops and crashes lose DRAM-only records; memory copies remain until their spills finish. Non-pressure write failures receive one retry. Delete the unused TierStore::cascade hook, forwarder and callers. Production net: -21 lines, excluding tests; all-file net: -41 lines. Branch validation: CI-exact locked release no-run builds for Selene and manager with test-support, Helios server/runtime-package lib and Flare pass. Both RDMA test checks pass. Full suites ran once: Selene 350 passed; manager 301 passed. GPU serving and latency comparisons were not run. Design-Signed-Off-By: yatesdr; design-review=1c01
Instance and manager max age default off. Enabled expiry refreshes last use through the existing ancestor walk without changing ancestor LRU positions. Preserve ordinary per-key LRU order and the manager's store-wide access counter. Duplicate publication refreshes last use only in memory, disk, both composite Present paths and the manager, matching their unchanged LRU order at the base. Composite lookup touches only the disk index, as before. Each index selects at most 64 expired leaves per minute, stopping at the first fresh leaf; the manager sweeps every namespace. Reuse cascading removal, unlinking and memory release. Fetch completion refreshes ancestry once. Remove timestamp jobs, completion paths, extra pins, the timestamp job kind, mtime rank relay, modified state and stamp_due rate limiting. The ancestor walk no longer needs a callback. Recovery retains the existing file-mtime scan. After a restart, disk records reused but never rewritten can expire up to one max age early: recomputation, never wrong output. Document this for both stores. Production net: +194 lines, excluding tests; all-file net: +602 lines. Branch validation: CI-exact locked release no-run builds for Selene and manager with test-support, Helios server/runtime-package lib and Flare pass. Both RDMA test checks pass. Full suites ran once: Selene 350 passed; manager 301 passed. Coverage includes ancestry, bounded expiry, two manager namespaces, resident memory copies of disk records, unchanged hit mtimes and mtime recovery. GPU serving and latency comparisons were not run. Design-Signed-Off-By: yatesdr; design-review=1c01
Keep flare_host_cache_retry_abandoned_total and add a bounded reason label. Carry the retry cause through Deferred events and the existing retry list; include terminal oversize and I/O losses. Extend the existing metrics paths with per-tier expiry and actual row allocation/capacity. The manager exposes its expiry totals through metrics_text. Count expired removals only as expiry, keeping pressure evictions separate through the existing accounting path. Production net: +44 lines, excluding tests; all-file net: +109 lines. Cumulative production net: +232 lines, excluding tests. Branch validation: CI-exact locked release no-run builds for Selene and manager with test-support, Helios server/runtime-package lib and Flare pass. Both RDMA test checks pass. Full suites ran once: Selene 350 passed; manager 301 passed. GPU serving and latency comparisons were not run. Design-Signed-Off-By: yatesdr; design-review=1c01
Record each fetch's parallel disk bytes over the wall time from the first read start to the last completion in each wave, using the existing rate estimator. Reset the timing fields after sampling so a later wave does not sample the same bytes again. Remove per-node timestamps that priced a parallel chain at one read's bandwidth. Cover two concurrent record reads and their combined bytes per interval. Validation: CI-exact locked release no-run builds for Selene and manager with test-support, Helios server/runtime-package lib and Flare; both RDMA checks; and the Selene and manager suites pass. The manager suite was rerun with 4 GiB of container shared memory after exhausting the default 64 MiB. One client fixture failed to start its manager and passed on an isolated retry. GPU serving and performance comparisons were not run. Design-Signed-Off-By: yatesdr; design-review=1c01
Move the existing disk-store reopen retry into one shared test helper and use it at every test-side directory-lock acquisition. Parallel subprocesses can briefly inherit another test's flock between fork and exec. Retry only WouldBlock, sleeping 1 ms between attempts for up to 5 seconds; return every other error immediately. Production acquisitions stay unchanged. Production net: 0 lines; all-file net: +12 lines. Validation: locked release no-run builds for Selene and manager with test-support, Helios server/runtime-package lib and Flare pass, as do both RDMA test checks. Before this test-only change, full suites passed once: Selene 350, manager 301. After it, all ten full Selene runs with test-support passed: 350 tests each, 3,500 total, zero failures. The changed manager and Helios test callers compile. GPU tests and serving comparisons were not run. Design-Signed-Off-By: yatesdr; design-review=1c01
Host memory was two fixed slab classes, a quarter for rows and the rest for pages, so a row-heavy model filled its rows while most page memory sat empty. Rows now occupy as many page-sized slots as they need, laid out per rank in each rank's share of the slot exactly as pages are, so one resolver serves split, registered and pool addresses. CPU readers walk a row's pieces in record order, and the row checksum streams over them, so the on-disk record format and checksum values are unchanged and existing records recover. Compaction may take the coldest single-child interior row. The ratio sizing and per-class minimums are removed. Design-Signed-Off-By: yatesdr; design-review=1c01
The disk tier kept its own fixed staging pool and copier mapping beside the memory pool. Both now draw slots from one mapping and one registration; only the free list sits behind a lock, so address lookups on the copy path take none. Memory and disk keep independent copiers and the existing close order, and memory maintenance never waits on staging. A copier that fails to close leaves the mapping leaked rather than reused. Design-Signed-Off-By: yatesdr; design-review=1c01
Count a record's slots, row pieces included, wherever memory is spilled or retired, and keep one slot count for headroom, retirement and held demand. Allocate and release a record's slots in one call under one lock, resolve slot addresses and per-record slot counts in one place each, and carry the warm-tier size in the prepared plan instead of a pool flag. Composites require staging to share the memory pool. A failed copier close now keeps the mapping by forgetting one handle. Per-rank parts no longer carry a separate region type, and unreachable guards and a dead parameter are removed. Design-Signed-Off-By: yatesdr; design-review=1c01
Host memory is the warm tier plus an intake sized at startup from the layout and the selected concurrency: room for two rows per active request and two transfer windows, shrunk to fit the host budget rather than refused. Memory allocation keeps the intake free by evicting the coldest clean records on demand, so staging never waits on a warm tier full of durable copies, and the coldest dirty records are written once free plus clean slots fall below two intakes. Clean slots are counted as records change state, not by scanning. The fixed staging pool and its constants are removed. Design-Signed-Off-By: yatesdr; design-review=1c01
Cover warm or no warm, disk or no disk, and pool or no pool: each publishes, evicts, restores byte-identically and drains. Multi-host tests check that a peer stages through the host intake with multi-piece rows, that a warm tier is refused, that pool-only allocates no local pool, and that nothing configured stays disabled. The tier dispatch itself needed no change. Design-Signed-Off-By: yatesdr; design-review=1c01
A store built with no intake never reached the clean floor, so a pooled memory tier filled without retiring records to the manager. Memory and staged stores now raise the intake to two full transfer windows in one place, and test fixtures size their pools through it. Retirement keeps its single rule. Design-Signed-Off-By: yatesdr; design-review=1c01
With one slot class the rows-against-concurrency warning no longer means anything. Startup now warns once, and only when the host budget forced the intake below its requested size, naming the memory it needs. Design-Signed-Off-By: yatesdr; design-review=1c01
Host-cache metrics report slots used, slots total and the intake instead of row capacity. The help text and prefix-cache documentation describe one warm-tier setting (0 allowed), the automatic intake, every tier combination, the cost of multi-piece rows and the larger intake that disk-only instances now reserve. Design-Signed-Off-By: yatesdr; design-review=1c01
A memory-only store refused a record as Oversize only when it exceeded the warm slots, but allocation also keeps the intake free, so a record between the two retried forever. Fit and shortfall are now one pair of functions, used by every caller, with one evictable predicate. Chain retention and spill selection no longer scan every record when nothing is short. The intake is fixed when the mapping is built. Forwarding accessors and a dead row branch go, helios resolves only its own rank's row pieces, and slot wording replaces the old slab terms. Tests cover three-slot rows across the tier matrix and a two-rank record written by the previous format, and the large-pool property test no longer starves concurrent tests. Design-Signed-Off-By: yatesdr; design-review=1c01
Record files are written O_DIRECT into a fresh temporary file that was never sized, so every write extended it. On ext4 an extending direct write holds the inode lock exclusively until it completes, which serialized the disk ring's chunks at queue depth one; one lost NVMe completion interrupt then froze the whole pipeline for the kernel's 30 s I/O timeout. Create now allocates the record's final length with fallocate (Linux only; a filesystem without it behaves as before). With the blocks allocated every direct write is an overwrite, which ext4 runs under the shared lock, so the writes overlap; a sparse set_len would not help. If preallocation fails, the temporary is removed before the error is returned. The commit path never relied on the temporary's size: the final write is the state row at the end of that length, and the renamed file keeps it. Validation: selene unit tests pass (362), including a new check that a created file has its final length. On a host whose record NVMe loses completion interrupts, DeepSeek 3k c32 disk-only: the unfixed build abandoned 376, 0, 69 and 350 publications in four runs with up to 63 writers parked on the inode lock; the fixed build abandoned none in eight runs (restored 128/128 each), including seven runs that hit an NVMe completion timeout, with no writer parked on the lock (mean write depth about 5.5). Design-Signed-Off-By: yatesdr; design-review=1c01
Invariant: a record's file is written only after its parent's file has
committed, so no record becomes ready on disk under an unwritten parent.
Parked records spill to disk only under memory pressure, coldest first. A
hot shared ancestor (the root every conversation shares) is never the
coldest, so it stayed parked for the whole run while its children were
written. The disk then held ready children whose chain was incomplete.
Symptom: a republished checkpoint at such a key got Retry(Inflight) and was
retried until its GPU snapshot was evicted, then counted as
retry_abandoned{inflight}. One key was retried 305 times, and a burst run
counted 16-19 retry_abandoned{inflight} with the content still held.
spill() now writes the root-most ancestor that has not committed instead of
the record, and returns the key whose write it started (None when nothing
started). The record waits until that file has committed. spill_parked
counts writes in flight as before and adds the started key's slots, so
ancestors started for a cold child count toward the pressure target and the
in-flight cap (MAX_SPILLS). store_status and the lookup predicate are
unchanged.
Shutdown drain: when the record's ancestor is not yet written, the drain
starts the ancestor and waits for it with its existing in-flight wait. If
the ancestor cannot start (no reservation, write refused), the record is
given up and counted as skipped rather than retried until the timeout.
A parked record whose final write fails (the second failure), or whose
file fails to commit, now also drops the parked records under it, since
their parent will never reach disk.
Validation: selene unit tests pass (366) on cn4, including four new tests
(a cold child waits for its parked parent, then is complete and a republish
returns Present; ten cold children with hot parents start exactly
MAX_SPILLS parent writes and no child; the drain returns promptly when the
ancestor cannot start; a parent that fails twice takes its parked child
with it). The drain and parent-failure tests fail without the change.
DeepSeek 3k c32 n=128 on 4 GPUs: 7 of 7 runs abandoned 0 in the burst and
probe phases and restored 128/128. No restart benefit is claimed: a restart
without the shutdown drain restored the same 62 full / 65 partial / 1
recomputed on this build and on the unfixed one.
Design-Signed-Off-By: yatesdr; design-review=1c01
yatesdr
force-pushed
the
selene-eviction
branch
from
November 1, 2026 12:32
67752e5 to
e7b35fd
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
This adds eviction and backpressure for Selene's host tiers. It stops checkpoints being lost between the GPU and host memory under pressure, replaces the fixed row and page slabs with one pool of equal slots plus an automatic intake, adds an optional maximum record age, and makes local mode write to disk only when memory needs the room.
It also contains two fixes found while validating on a disk-heavy workload: record files are preallocated so direct writes overlap, and a parked record's ancestors are written to disk before it.
Changes
--prefix-cache-drain-on-shutdownnow covers local mode.--prefix-cache-max-age DURATION,[prefix_cache] max_age, or the manager's--max-age. It is off by default, and off means no extra work.host_cache_expired_total{tier}, areasonlabel onretry_abandoned, and slot metrics (slots in use, slots total, intake slots) in place of the row gauge.Behaviour changes
--prefix-cache-drain-on-shutdownto keep them.Testing
DeepSeek V4.1 Flash, TP4, on an SM120 host with 4x RTX PRO 6000. Bursts are 3k-token conversations, 32 concurrent, 128 per run. "Abandoned" means publications lost between the GPU and the host.
inflightabandons per burst run)The slot pool, intake and soak results were measured on the revision before the two fixes; the fixes change only record-file creation, spill order, the shutdown drain and the drop of a failed parent's parked children. The disk-only and 8G burst rows above were rerun on the final revision.
page_only_nodes_supply_pages_but_never_matchtest passes unmodified.Notes