Repository navigation
feat(umbp): converge eviction onto one path; nodes evict locally by default - #741
Conversation
Eviction is decided in three places (master EvictKey, PageBackend's masterless round, PeerSsdManager's after-write round) and freed in two more ways than the medium's Evict (the page round's own copy of it, and the SPDK daemon's EvictLRU). The master's request also drops the tier it measured, so the peer frees by placement and can delete copies nobody asked about. The doc lays out the target: the master names (key, tier); PeerPool::Evict is the one routing entry point both the master and a background local eviction worker call; MediumBackend::Evict is the only code that frees a committed key; nothing evicts on the put path. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rees that copy
The master picks victims per (node, tier) but shipped only keys, so the
peer freed by placement: on a tier without on_evict offload it deleted the
key from every backend, and with copy-mode promotion (key in DRAM and SSD)
SSD pressure freed the DRAM copy and left SSD as full as before.
- EvictionVictim{key, tier}: SelectVictims and EvictKeyDispatcher carry
the tier each victim was charged to.
- EvictKeyRequest gains `repeated TierType tiers = 2`, parallel to keys.
An older peer ignores it; an older master sends none and the peer frees
each key from the medium that currently holds it.
- PeerPool::Evict takes PoolEvictRequest{key, tier, backend_id} and frees
exactly one copy: demote it when it is the placement and its tier has a
downstream (any trigger -- on_evict vs watermark now only says whether
the pool also offloads proactively), otherwise drop that copy only. Drops
run outside the pool lock; lifecycle_mutex_ keeps ClearLocal out.
- A transition that lands on a parked (draining) copy promotes it to the
durable one, so a later eviction cannot delete it as still-draining.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rough PeerPool::Evict
Before this, a committed key could be freed four ways: the master's
EvictKey (through PeerPool and the medium's Evict), PageBackend's
masterless round (its own copy of Evict's body, inline on commit),
PeerSsdManager's round (inline after every SSD write, plus
evict-then-retry on a failed write), and tier transitions. The two
inline rounds bypassed PeerPool -- no on_evict demotion, stale placement,
invisible to InstrumentedBackend -- and the page round ran before the
pool's watermark check, so on a masterless node it deleted the cold keys
the watermark offload was about to move.
Now:
- MediumBackend::Evict is the only code that frees a committed key for
capacity. PageBackend::MaybeEvictToLowWatermark and
PeerSsdManager::EvictToLowWatermark (with its watermarks, after-write
checks and evict-and-retry) are gone; a write that does not fit fails.
- MediumBackend::EnableLocalEviction is replaced by ordering hooks that
free nothing: TrackEvictionOrder() and EvictionCandidates(max) ->
EvictionOffer{key, bytes}, coldest first, skipping leased/pinned keys.
- PeerPool::EnableLocalEviction(backend, high, low) starts one background
worker. Commits, NO_SPACE allocations and finished transitions only wake
it; it takes offers until their bytes cover the overage and calls
PeerPool::Evict({key, backend_id}, kReclaim) -- the call the master's
EvictKey makes -- so a tier with a downstream demotes instead of
deleting. Lowest tier first, 64 keys per call, with a fixed byte budget
as well as the live usage check.
- PoolClient enables it for every medium without a master (paged media at
local_evict_*, SSD at its own watermarks) and for SSD with a master, as
before.
- A demotion that loses a race to another transition no longer falls back
to deleting the source, which could abort that transition and lose the
key.
- mori_umbp_client_local_evict_total{event=round|key|no_candidate|stalled};
the SSD medium's eviction_round event is gone (rounds are the pool's,
keys and bytes are InstrumentedBackend's).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With ssd_backend=spdk, PeerSsdManager reaches the device through the spdk_proxy daemon, whose SpdkSsdTier retried a failed allocation by dropping its own oldest keys (EvictLRU). Nothing told PeerSsdManager: it kept listing those keys, no REMOVE reached the master, and the master kept routing reads to bytes that were gone. Fragmentation could trigger it well below the SSD watermark. A write that does not fit now fails, as it does on the file tier, and freeing space is the owner's local eviction, which reaches the tier through Evict(). PeerSsdManager is the daemon's only client. Not built by the default dev image (no SPDK); checked with a syntax-only compile against 3rdparty/spdk/include. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The PR description carries the design; the two code comments that pointed at the doc already explain themselves. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Same coverage, fewer lines: - the SSD- and DRAM-scoped evicts of a promoted key share one test, as do the stale-tier and backend-scoped evicts; - the read-recency check rides on the drain test, and the bad-config check on the unordered-medium one; - the mismatched-tier-count case rides on the unscoped EvictKey test; - the two "writes never evict" cases share one fill; - dropped VictimCarriesTheTierItWasChargedTo (KeyOverBudgetInTwoTiers checks the tier on both victims) and the master-chain SSD-pressure test (the parked-source chain test checks the SSD victim end to end, and TierScopedEvictFreesOnlyThatMediumsCopy what the pool does with it); - removed the now-unused Keys() and HasRemove() helpers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Folds #742 into this PR. - Master: a round frees each (node, tier)'s overage in bytes, optionally capped by UMBP_EVICT_MAX_BYTES_PER_ROUND, instead of 32 candidates per bucket -- which freed a few MB per round and took hours to drain a 512 GiB tier's 0.9 -> 0.7 band. The store returns every eligible row and the strategy trims to the budget. - Peer: PeerPool::Evict queues a demotion on the transition worker instead of running the copy inside the EvictKey RPC, so the call never waits on bytes. Each result carries an outcome (freed, demotion queued, in flight). The worker runs an eviction's demotion whatever the tier's own watermark says, and drops the copy if nothing downstream can take it. - Local eviction counts queued demotions toward its budget (a migration pin no longer hides a key from EvictionCandidates), so a pass never queues more than the overage while the copies run. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… tests Comments: drop history and restated rationale (the commit message has it). Tests, same coverage with fewer of them: - The two GetEnvUint64 tests become one. - Drop LruMasterEvictStrategy.ABudgetLargerThanAnyRowCapIsMetInOneRound: the strategy never had a row cap (the manager did), and it is unchanged. - The three MasterEvictionBytes tests become one, which also reads the limit through UMBP_EVICT_MAX_BYTES_PER_ROUND. - EvictReturnsBeforeTheDemotionCopyRuns folds into OnEvictMigratesBytesToConfiguredBackend, and QueuedDemotionsCountTowardTheBudget into the local-eviction demotion test (now ATierWithADownstreamDemotesJustTheOverage). - GatedEngine's hold times out after 10 s, so it needs no ReleaseOnExit guard to keep a failed test from hanging teardown. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
7e49f72 to
f19c52b
Compare
Comment-only. Drops narrative and rationale that restates the code, and the same "the pool decides, the medium only supplies order" paragraph that was repeated across several headers; field and helper docs become trailing one-liners. Added comment lines across the PR: ~400 -> 195. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With a master, a node can evict at its own watermarks instead of waiting for the master's EvictKey rounds, while still taking its victims from the master's cluster-wide recency. - Master: new GetEvictionCandidates RPC returns a node's keys on one medium (optionally one logical tier), least recently accessed first, until their sizes cover the requested bytes; master-leased keys are skipped. RegisterClient and every Heartbeat carry peer_driven_eviction, and EvictionManager leaves such nodes out of its rounds (in memory only). ClientRegistryConfig::advertise_peer_driven_eviction gates both. - Peer: local eviction asks a candidate source first (at most once per 200 ms per backend), rounds its value sizes up to the backend's pages, evicts through PeerPool::Evict, and covers the rest from the medium's own LRU. In peer-driven mode PoolClient runs local eviction for every medium, at a tiered policy's per-tier watermarks when there is one. - Opt-in: PoolClientConfig::peer_driven_eviction or UMBP_PEER_DRIVEN_EVICTION=1, and only once the master confirms support; otherwise behaviour is unchanged. New metric event master_key. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Replaces the peer_driven_eviction switch with PoolClientConfig::eviction_mode (env UMBP_EVICTION_MODE), which says how a node with a master evicts: - local (default): every medium at the node's own watermarks, by its own LRU, without asking the master. - master_ranked: the same, with victims ranked by the master (GetEvictionCandidates), falling back to local LRU. - master: DRAM/HBM wait for the master's EvictKey rounds; SSD still evicts locally (the previous behaviour). A self-evicting node (local or master_ranked) asks the master to leave it out of its rounds; every mode still serves EvictKey, so the master can evict on demand. A node without a master evicts locally as before. Self-evicting nodes use a tiered policy's per-tier watermarks when there is one. Tests that fill tiny tiers on purpose (cross-node smoke, tier benchmark) now run in master mode, the behaviour they were written against. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
End-to-end validation of the eviction PR chain: passed. Branch
The log signatures of the 7 bugs this e2e originally found all stay at 0: whole-batch staging failures, Notes:
🤖 Generated with Claude Code |
The master ranks a node's keys by last_accessed_at, which only RouteGet updates. A local_first node serves its hits without the master, so under master_ranked (and master) the hottest keys looked the coldest and went first: in a 4 GB DRAM / 8 GB SSD smoke a hot set re-read throughout the fill survived 0/512 under master_ranked vs 512/512 under local, and the hot reads then served from SSD slowed demotion until DRAM sat at 100%. A node whose eviction follows the master's recency now ships the keys it reads locally (deduped, bounded) on its heartbeats, and the master records an access for each. Local mode sends nothing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…al-leases Picks up #741's local-read reports; ResolveLocalBatch keeps this branch's probe release and deferred keys alongside them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Picks up #741's local-read reports. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
master_ranked asked GetEvictionCandidates synchronously inside the eviction pass, at most every 200 ms per backend. The in-memory master answers by scanning its whole index; at e2e scale (500k DRAM + 2.7M SSD keys on one node) that is 0.66 s per DRAM query and 1.9 s per SSD query, so a DRAM pass stalled most of every second and every SSD query hit the 1 s deadline and was thrown away after the master had done the work. The pass now waits at most 20 ms for the query it sends (a small index still answers the same pass); a slower answer serves a later pass unless it is over 3 s old, and the medium's own order evicts meanwhile. One query per backend is in flight at a time. The deadline goes to 3 s to match. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…al-leases Picks up #741's non-blocking ranked queries. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Picks up #741's non-blocking ranked queries. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
One pass reclaims a backend's whole watermark band: in the e2e a single DRAM pass covered 108k keys, while one GetEvictionCandidates answer carries at most 4096 and arrives after the pass has moved on. The pass asked once, at its start, so master_ranked ranked 0 of those 108k victims (at most 4% even when it used to wait for the answer). The pass now takes the master's answer whenever it is ready, and asks again for what the budget still needs as each answer runs out (still at most one query per 200 ms per backend); the medium's own order evicts in between. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…al-leases Picks up #741's whole-pass master ranking. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Picks up #741's whole-pass master ranking. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflict in page_backend: keep main's side. #741 deleted PageBackend's own watermark loop (eviction now goes through PeerPool::Evict, so the backend's op="evict" already counts it), so this branch's PageBackend::SampleMetrics, its four local_evict_* counters, kPageBackendLocalEvictEvent and the activity summary's special case for them are dropped. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Summary
Converges UMBP eviction onto one path. Whoever decides a key must go — the master, or the node itself — calls
PeerPool::Evict, and the bytes are freed by that medium's singleEvict()implementation. Nothing evicts inline on a put.It also folds in two follow-ups:
Rebased on
main(after #754). Please squash on merge: the branch adds the design doc in its first commit and deletes it again.Why
A committed key could be freed five ways, and only the master's request went through
PeerPool:EvictKeyon_evicttier it deleted the key from every backend, and with copy-mode promotion (key in DRAM + SSD) SSD pressure freed the DRAM copy and left SSD full.PageBackendmasterless roundEvict's body, inline on commit. Ran before the pool's watermark check, so it deleted the cold keyswatermarkoffload was about to move to SSD. Invisible toInstrumentedBackend.PeerSsdManagerroundPeerPool.SpdkSsdTier::EvictLRUPeerSsdManager— it kept listing the keys, and the master kept routing reads to bytes that were gone.The master's own rounds were also slow and coarse:
(node, tier)every 5 s, about 4 MB/s at the 512 GiB deploymenteviction_manager.cppcites. Draining one 0.9 → 0.7 band took about 6.7 hours.Changes
1. Per-tier evict requests (
4254bd052)EvictionVictim{key, tier}:MasterEvictStrategy::SelectVictimsandEvictKeyDispatchercarry the tier each victim was charged to.EvictKeyRequestgainsrepeated TierType tiers = 2, parallel tokeys. Old peers ignore it; an old master sends none and the peer frees the copy that currently holds each key.PeerPool::Evict(PoolEvictRequest{key, tier, backend_id})frees exactly one copy: demote it if it is the placement and its tier has a downstream (any trigger), else drop that copy only. Drops run outside the pool lock;lifecycle_mutex_keepsClearLocalout.2. One eviction implementation per medium (
5da726828,3a0621f64)MediumBackend::Evictis the only code that frees a committed key for capacity.PageBackend::MaybeEvictToLowWatermarkandPeerSsdManager::EvictToLowWatermark(with its watermarks and evict-and-retry) are removed; a write that does not fit fails.MediumBackend::EnableLocalEvictionis replaced by ordering hooks that free nothing:TrackEvictionOrder()andEvictionCandidates(max)→EvictionOffer{key, bytes}, coldest first, skipping leased/pinned keys.3. Local watermark eviction through the same call (
5da726828)PeerPool::EnableLocalEviction(backend, high, low)starts one background worker. Commits, NO_SPACE allocations and finished transitions only wake it.PeerPool::Evict({key, backend_id}, kReclaim), the same callEvictKeymakes, so a tier with a downstream demotes instead of deleting. Lowest tier first, ≤64 keys per call, bounded by a byte budget as well as live usage.mori_umbp_client_local_evict_total{event=round|key|no_candidate|stalled|master_key}; the SSD medium'seviction_roundevent is removed.4. Byte-sized rounds and asynchronous demotion (
d0faaa820, formerly #742)evict_batch_size(32 rows) is replaced bymax_evict_bytes_per_round(envUMBP_EVICT_MAX_BYTES_PER_ROUND, default0= the whole overage above the low watermark). The store returns every eligible row and the strategy trims to the byte budget.PeerPool::Evictqueues a demotion on the transition worker instead of copying inside the call, so neitherEvictKeynor the local worker waits on data movement. It returnsPoolEvictResult{key, bytes_freed, outcome}, where the outcome is one ofkFreed,kDemotionQueued,kInFlightorkNone.5. Eviction modes (
34e67ed26,0af37b45f, formerly #760;master_rankedfixes from e2e testing458e3e914,3935422db,e8b950a44)Each node chooses how it evicts when it has a master:
PoolClientConfig::eviction_mode, envUMBP_EVICTION_MODE.local(default)master_rankedGetEvictionCandidatesRPC: this node's keys on that medium (optionally one logical tier), least recently accessed first, master-leased keys skipped. Falls back to local LRU.masterEvictKeyrounds (SSD still local)peer_driven_evictionon register and on every heartbeat, and the master'sEvictionManagerleaves it out of its rounds. The master keeps the flag in memory only, so there is no store schema change.EvictKeystays: every mode still serves it, so the master (or a future user-facing API) can evict on demand.HeartbeatRequest.local_reads, deduped, at most 64k per beat), and the master records an access for each. Without this alocal_firstnode's hits never reached the master's recency, so its hottest keys ranked coldest (458e3e914).mastermode reports them too, since its rounds rank by the same recency.3935422db): it waits at most 20 ms for its first query, and takes a slower answer when it arrives if it is under 3 s old. One query per backend is in flight, at most every 200 ms; the RPC deadline is 3 s. At e2e scale the in-memory master needs ~0.6 s per DRAM query and ~1.9 s per SSD query (it scans its whole index).e8b950a44).master_rankedfalls back to local LRU.Behavior changes
local); setUMBP_EVICTION_MODE=masterfor the old behaviourwatermark-trigger tier with a downstream(node, tier)per 5 sUMBP_EVICT_MAX_BYTES_PER_ROUNDEvictKeyon a demoting tierTest plan
tests/cpp/umbpsuite (53 tests) inrocm/mori-devwith RDMA: 51/53. The two failures are pre-existing onmain, and both passed 3 of 3 run on their own:umbp_cross_node_smoke: RDMAdefaultContextassertion underctest -j 8.umbp_pool_client_ranges: the GPU gather intermittently reads back zeros.PeerPoolLocalEviction.*30/30,MasterPeerDrivenEviction.*30/30, and the end-to-end eviction-mode tests 5/5.mainat097bb470fwith the master-ranked commit on top: it applied cleanly and the suite passed 52/53. Not re-run after the eviction-modes commit.tiers.PeerPool::Evict: tier, backend, absent copy, watermark-tier demotion, full-downstream drop.watermarkoffload, and rejects invalid config.EvictreturnskDemotionQueuedwhile the copy is still held, and a repeat call returnskInFlight.EvictKeyrounds.test_umbp_peer_driven_eviction, real master + DRAM node over gRPC):localevicts within 3 s without asking the master;master_rankedtakes all 7 victims from the master, and keys the node read locally rank behind every other;master_rankedfalls back to local;masterleaves DRAM to the master's rounds.SsdBackendoffers candidates and SSD drains through the pool;PeerSsdManagerno longer evicts on writes.umbp_cross_node_smoke,umbp_tier_benchmark) now run inmastermode, the behaviour they were written against.3a0621f64): only syntax-checked against3rdparty/spdk/include; needs an SPDK-enabled build.BUILD_OPS/PYBINDS=OFF; the bindings reference none of the changed APIs.local(default): the whole chain (feat(umbp): converge eviction onto one path; nodes evict locally by default #741 + fix(umbp): SSD staging no longer fails local loads; a local read holds its bytes until it is done #762 + fix(umbp): retry eviction demotions instead of dropping the only copy; count what is dropped #763 + feat(umbp): serve metrics from a masterless node, and summarise offload/load/evict/promote/demote at INFO #683) ran the InferenceY DSv4 agentic recipe on n06-21 with DRAM 300 GB → SSD 1.5 TB (test/umbp-evict-e2e-v2@2e1663d91): 283,993 tok/s, 0 failed loads, 0 dropped demotions, all old-bug signatures at 0.master_ranked(a master beside the standalone server; found the three fixes above):localis unchanged.c160d07ef(fixes 1–2): completed, with 0 GPU faults, 0 dropped demotions and no put failures. But the master ranked 0 of 913k victims, which led to fix 3.b8d526f23): pending.Follow-ups (not in this PR)
master_rankedcheap: a master-side per-(node, tier)LRU index. Today eachGetEvictionCandidatesscans the master's index (every key in the in-memory store), the same work as one master round.localis still open. Its recency is per key, not per copy, so it can't tell which copy (DRAM or SSD) is the hot one.mastermode now.PageBackendlocal-evict counters conflict with this branch, which moves those counters to the pool.🤖 Generated with Claude Code