Repository navigation
[Store] Prefetch SSD-only objects to DRAM on exist - #4272
Conversation
Add Client::QueryReadOnly / BatchQueryReadOnly (and the MasterClient counterparts GetReplicaListReadOnly / BatchGetReplicaListReadOnly), backed by the existing admin read-only master handlers GetReplicaListForAdmin / BatchGetReplicaListForAdmin, which are now also registered on the master RPC server. The read-only path grants no lease, does not trigger promotion-on-hit, and does not update valid_get metrics; the returned QueryResult pins the lease expiry to the query start time so callers cannot mistake the metadata for a lease-protected one. Intended for best-effort background paths (e.g. SSD prefetch metadata classification) that must not perturb hot-path state. Wire compatibility: no existing RPC signature changes. The two admin handlers are purely additive registrations; older peers are unaffected. Adds client_readonly_query_test covering replica-equivalence with the lease-granting path, no-lease semantics, missing-key errors, and batch ordering.
A memory replica whose transport endpoint matches one of this client's mounted segments (by segment name or TE endpoint) was misclassified as remote whenever the endpoint differed from local_hostname_ / GetTransportEndpoint(). That made the local hot-cache submit path and the promotion source check skip replicas that are in fact local. Keep the two lock-free fast paths; fall back to a scan of mounted_segments_ under mounted_segments_mutex_ only when both miss.
Per-client throttle state for the SSD prefetch-on-exist path: a sharded per-key dedup/rate-limit table (kTriggered -> kInFlight -> kCompleted, with kFailed / kAlreadyResident / kDelegated as terminal non-blocking states) plus a memory-pressure cooldown that backs prefetch off when the master runs out of DRAM, so prefetch stops competing with eviction/offload. Design notes: - reserve() checks and registers atomically per shard; expiry is lazy (per-entry deadline + amortized per-shard sweep), so the hot probe path stays O(batch size) instead of scanning the whole table per call. - kFailed keys only block re-triggering for the (shorter) cooldown window, so transient failures retry quickly; kDelegated keys (handed to a remote holder) block for the full TTL and are never waited on locally. - waitForCompletion() exits immediately on terminal states instead of burning the caller's budget. - Adds the enable_ssd_prefetch / ssd_prefetch_cooldown_sec / ssd_prefetch_dedup_ttl_sec / ssd_get_wait_ms config keys (all default off/unchanged behavior) and the PROMOTION_ALREADY_EXISTS error code.
Master-side support for SSD prefetch: RegisterPrefetchTask records a promotion task for the caller's tenant without the promotion-on-hit admission gates (frequency sketch, DRAM watermark) and without pushing to the holder's heartbeat mailbox - the prefetch caller drives the execution chain (PromotionAllocStart -> ... -> NotifyPromotionSuccess) directly. Semantics: - Holder-only: the caller must own the source LOCAL_DISK replica (holder_id == client_id), mirroring PromotionAllocStart's gate. - Shares promotion_in_flight_/promotion_queue_limit_ with promotion-on-hit, since both compete for the same DRAM. Dedup against the same per-key promotion_tasks entry makes double promotion impossible across the two paths. - Distinct idempotency: an existing MEMORY replica or in-flight task returns PROMOTION_ALREADY_EXISTS, a normal skip outcome for best-effort prefetch rather than a failure. - NotifyPromotionSuccess grants the usual default_kv_lease_ttl read lease for from_prefetch tasks so the promoted DRAM replica survives until the follow-up get(). The RPC is additive; older masters reject it and prefetch degrades to a no-op. No existing RPC signature changes. Adds prefetch_task_master_test covering register success/duplicate, missing key, non-holder rejection, memory-resident skip, and the from_prefetch lease grant. Co-authored-by: h30027576 <huangdong51@huawei.com>
… chain Holder-side execution for SSD prefetch. The per-key promotion chain (PromotionAllocStart -> staging AllocateBatch -> BatchLoad -> PromotionWrite -> NotifyPromotionSuccess) is extracted from ProcessPromotionTasks into PromoteOneKeyFromLocalDisk, now shared by promotion-on-hit and prefetch; the master-side failure release becomes an RAII guard (PromotionStateGuard) covering every failure path past AllocStart. PrefetchKeys runs that chain per key, best-effort: per-key failures are reported via callback and counted on SsdMetric (prefetch_complete_total / prefetch_fail_total), and PromotionAllocStart NO_AVAILABLE_HANDLE sets a dram_pressure out-flag so the caller can back off via the throttle cooldown. LookupLocalObjectSize sizes staging from the storage backend's authoritative metadata (BucketStorageBackend::GetObjectDataSize), never from a remote caller's hint. No behavior change for promotion-on-hit (same chain, same log sequence); nothing calls PrefetchKeys yet - wiring arrives with the exist/get integration. Design doc: docs/source/design/ssd-prefetch.md. Co-authored-by: h30027576 <huangdong51@huawei.com>
SsdPrefetcher drives the best-effort promotion. TriggerPrefetch dedups through PrefetchThrottle (before any RPC, for local and remote-holder keys alike), then a bounded 4-thread pool batches read-only metadata queries (128-key chunks, pipelined), classifies SSD-only keys, and promotes local ones via RegisterPrefetchTask + FileStorage::PrefetchKeys or delegates to remote holders via the new additive prefetch_offload_object RPC (old peers reject it; keys stay SSD-only). Only a process that actually holds the SSD object registers the task (LookupLocalObjectSize first), so a metadata-only EngineCore cannot leave a phantom PROCESSING replica that the get path would wait on. Exist path: is_exist/batch_is_exist with ExistOptions.prefetch_to_memory triggers the above; the synchronous path is a dedup map lookup plus a pool enqueue - no added RPC, and a complete no-op unless enable_ssd_prefetch is set (default off). Get path, enabled by ssd_get_wait_ms > 0 (default 0 = off): - one deadline is shared across the whole batch get, rather than a per-key timeout (a per-key budget multiplied a 16k prefix's tens of keys into tens of seconds of p99); - the get path never starts promotion on its own, but a batch headed for SSD gets one demand-side kick (ignore_cooldown bypasses only the client-side throttle backoff, never a master-side gate; the dedup TTL still bounds the rate). A key is waited on only while a local promotion is in flight or just completed (kInFlight/kCompleted); triggered-but-queued, failed, already-resident, and remotely delegated keys fall through to the SSD read immediately; - re-queries are read-only (no lease, no metrics); on a COMPLETE MEMORY replica the get is served from DRAM, otherwise the SSD read proceeds with no further delay. The prefetcher holds weak_ptrs injected at setup time, so teardown order is not a member-declaration-order assumption. Measured on DSv4-Flash + vllm-ascend (A2, 48 x 16385-token prefixes, fill -> overflow -> settle -> measure): at concurrency 2, TTFT median +5.2% / mean +5.1% / p99 +22.0%; at concurrency 4 under a deliberately DRAM-constrained store (2 GB, 97.5% full), TTFT median +85.2% / mean +81.4% / p99 +44.9%, versus SSD offload alone. Co-authored-by: h30027576 <huangdong51@huawei.com>
- store_c: mooncake_exist_options_t + *_is_exist_with_options variants (all-zero options == legacy behavior). - store_py: ExistOptions pybind class; is_exist/batch_is_exist take an optional keyword options argument (default: no prefetch). Prefetch knobs are passed by name through the dict-based setup (enable_ssd_prefetch, ssd_prefetch_cooldown_sec, ssd_prefetch_dedup_ttl_sec, ssd_get_wait_ms); the positional setup signature is unchanged. - DummyClient: accepts ExistOptions but ignores prefetch_to_memory with a VLOG note (prefetch needs the real client holding the SSD tier).
test_prefetch_on_exist.py covers the behavioral contract: an SSD-only key gains a MEMORY replica after is_exist(prefetch_to_memory=true) without any read, a subsequent get is bit-exact, and a plain is_exist does not promote (negative control). Wired into run_ssd_offload_smoke.sh with its own master instance running promotion_on_hit=false so the negative control is meaningful.
Add mermaid diagrams for the four load-bearing pieces, matching the code one-to-one: component architecture (SsdPrefetcher / PrefetchThrottle / FileStorage / MasterService), the end-to-end trigger flow, the throttle state machine with its per-state dedup windows, the RegisterPrefetchTask decision flow, and the shared promotion execution chain.
Remove constructs that break mermaid parsing: parenthesized comments and bracketed array forms in class members, asterisks and slash-separated aliases in method lists, parentheses in participant declarations, a stray double angle bracket in the decision flowchart, and semicolons inside node labels. Content unchanged.
Mechanical formatting from scripts/code_format.sh (18 clang-format wraps) and ruff (test_prefetch_on_exist.py). No behavior change.
Adapt to upstream changes since the PR opened: - MasterService header split (kvcache-ai#4127): PromotionTask::from_prefetch moves to object_runtime_state.h; friend list follows the MasterServiceTestPeer form (kvcache-ai#4135), plus PrefetchTaskMasterTest. - probeKey/BatchProbeKey RPCs (kvcache-ai#3801) coexist with ExistOptions. - Python unified-package layout (kvcache-ai#3586): keep buffer_pool.py at its pre-existing path; migration is a separate follow-up. - Design doc moved to docs/source/design/store/ssd-prefetch.md per the kvcache-ai#4196 module-grouped layout; drop stale toctree entries for moved docs.
fd36ce5 to
f4e20d9
Compare
Overall CommentThe mechanism layer is solid. One concern worth a second pair of eyes cc @fcczzz : the net benefit depends less on "DRAM utilization" than on whether the SSD-resident re-reference working set fits into DRAM headroom. That said, the feature is strictly opt-in (default off, complete no-op when off), so the blast radius is contained and I'm okay with it on that basis. I'd still like another pair of eyes on the saturated-regime question; if others share the concern, a follow-up with success-rate-aware backoff (or a configurable watermark gate) plus one "working set ≫ DRAM" benchmark point would close it. Inline commentsInline comments below — the three I care most about are the dead cross-process wait branch, the failure-path coupling with the on-hit retry pipeline (which also resets the execution-failure circuit breaker), and the missing early-exit in 1. 2. 3. Non-blocking4. 5. 6. 7. 8. |
Cover three findings from the review of kvcache-ai#4272, each red without the fix: - PrefetchFailureDoesNotRecordCandidate: a from_prefetch failure must not re-record an on-hit candidate (it leaks when promotion_on_hit is off, since the retry sweeper never runs). - RegisterPropagatesCandidateFailureCount: RegisterPrefetchTask must consume the candidate and propagate its execution-failure count onto the task, not re-arm the breaker with zero. - PrefetchKeysStopsBatchOnDramPressure: the first NO_AVAILABLE_HANDLE must stop the batch after a single alloc round-trip. - ZeroTtlKeepsNoPerKeyState: with dedup disabled the throttle stores no per-key entries (unsweepable, unbounded growth otherwise).
730b06e to
156293a
Compare
|
@zxpdemonio @alogfans Hi, could you help with the remaining code-owner reviews for this PR? The mooncake-wheel changes include the Python prefetch integration tests and a BufferPool compatibility entrypoint. The Transfer Engine change adds ascendcl linkage to shared_segment_test when USE_ASCEND_DIRECT is enabled. The Store-side reviews are complete. When you have a chance, could you review the changes in your areas and approve if everything looks good? Thanks! |
…d upstream content, move the integration test Three cleanups, no feature change: - Drop mooncake-wheel/mooncake/buffer_pool.py: upstream moved it to the unified python package long ago; a merge resurrected the legacy wheel copy, and nothing on this branch imports it. - Restore content from upstream PRs that were merged into main, entered this branch via merges, and were later rewound out of main: every path they touched is restored to main's version (or removed where main lacks it), keeping only this PR's own feature files in the diff. - Move the prefetch integration test to python/tests/ssd (the current test-tree convention) so the wheel tree carries no change; the smoke script's path follows.
02a25e4 to
46ae5ea
Compare
…efetch/cleanup-final Resolve against the re-landed upstream content: take main's versions of the eviction-index / namespace files (upstream's re-landed state), union the test registrations in tests/CMakeLists.txt, and normalize every remaining non-feature path to current main. Also make the moved prefetch integration test skip cleanly when no master is running: the wheel CI's pytest discovery runs python/tests/ssd/ with no services, and the store setup failed there with rc=-1. A TCP probe of MASTER_SERVER now skips the class outside the smoke environment.
46ae5ea to
3418124
Compare
SSD prefetch (kvcache-ai#4272) records its promotion task without a task id and runs it with the key-only PromotionAllocStart and NotifyPromotionSuccess, which a master with task ids refuses. Its late reports also have the problem task ids fix: a prefetch whose task has ended can still send its success or failure after a newer promotion of the key staged a buffer, and a key-only report would commit that buffer before its holder finished writing it, or free it while the holder writes into it. - RegisterPrefetchTask gives the task a new L4 task id, like a promotion-on-hit task, and returns it. The RPC gets a new name, RegisterPrefetchPromotionTask. A store that predates task ids calls RegisterPrefetchTask, which a master with task ids does not serve, so the master never records a task that such a store could not finish; a newer store calling an older master fails the same way. Either way prefetch does nothing. - FileStorage::PrefetchKeys takes each key's task id and runs the shared promotion chain with the task-named PromotionTaskAllocStart, NotifyPromotionTaskSuccess and NotifyPromotionTaskFailure. The prefetch master tests run the chain with the returned id and inject disk-only keys with AddReplica (a key-only NotifyOffloadSuccess registers nothing). New tests: every registration gets a new id, key-only calls cannot run a prefetch task, a late success or failure of an earlier prefetch neither commits nor frees the buffer of a newer one, and the store's prefetch calls echo the registered id.
### What this PR does / why we need it? Adds optional Mooncake SSD-to-DRAM prefetch on exist queries. When `enable_ssd_prefetch=true` in `mooncake.json`, `batch_is_exist` passes `ExistOptions(prefetch_to_memory=True)`, so Mooncake promotes SSD-only objects into DRAM during the cache probe, and a follow-up get is served from DRAM instead of SSD. Feature design lives in the Mooncake PR (kvcache-ai/Mooncake#4272); concept from kvcache-ai/Mooncake#2213. - Parse `enable_ssd_prefetch`, `ssd_prefetch_cooldown_sec`, and `ssd_prefetch_dedup_ttl_sec`; forward each throttle knob to `store.setup()` only when explicitly set, so omitted fields keep Mooncake defaults (5s backoff, 30s dedup TTL). - Capability probes keep this safe on older Mooncake builds: without `ExistOptions`, prefetch is disabled with a warning instead of failing initialization. - When a knob is forwarded, setup uses the config-dict overload; the store then owns its TransferEngine instead of sharing the global one. - Document the new `mooncake.json` fields in the kv-pool feature guide. ### Does this PR introduce _any_ user-facing change? Yes. New optional `mooncake.json` fields (`enable_ssd_prefetch`, `ssd_prefetch_cooldown_sec`, `ssd_prefetch_dedup_ttl_sec`). Behavior changes only when `enable_ssd_prefetch=true` and a compatible Mooncake build is installed; everything is off by default. ### How was this patch tested? Unit tests (148 pass): pytest -sv tests/ut/distributed/ascend_store/test_backend.py Covers config parsing (defaults / set / null), capability probes, prefetch resolve logic, both setup paths (keyword vs config-dict and the TransferEngine ownership that follows), and the `exists()` call path with and without prefetch options. End-to-end on Ascend A2 with the Mooncake PR head: TTFT A/B and SSD-offload smoke, evidence in the Mooncake PR. - vLLM main: vllm-project/vllm@544f182 Signed-off-by: DHX98 <haox.dai@gmail.com>
Title:
[Store] Prefetch SSD-only objects to DRAM on existDescription
With SSD offload enabled, a key whose MEMORY replica has been evicted keeps serving reads from a
LOCAL_DISKreplica — paid synchronously on the get() critical path. For workloads that probe withexistbefore reading (prefix cache lookup, async scheduling), the probe is a strong, early "will read soon" signal.This PR adds an opt-in, best-effort prefetch:
is_exist/batch_is_existwithExistOptions.prefetch_to_memory=truestarts an asynchronous SSD→DRAM promotion for keys that turn out SSD-only, so the follow-upget()can be served from DRAM. Promotion happens inside the probe→get queueing window; if it does not finish in time, or fails, or DRAM is saturated, behavior degrades to exactly today's SSD read. No correctness contract changes anywhere.Mechanism, briefly:
PrefetchThrottlewith TTL dedup and a DRAM-saturation cooldown).RegisterPrefetchTaskmaster RPC plus the same execution chain promotion-on-hit uses (PromotionAllocStart → AllocateBatch → BatchLoad → PromotionWrite → NotifyPromotionSuccess, extracted as a shared helper), or delegates to remote holders through a new additiveprefetch_offload_objectRPC.RegisterPrefetchTaskis tenant-aware, holder-checked (holder_id == client_id), shares thepromotion_in_flight_cap and the per-keypromotion_tasksentry with promotion-on-hit (double promotion is impossible), and reportsPROMOTION_ALREADY_EXISTSdistinctly so best-effort callers skip quietly.NotifyPromotionSuccessgrants the usual read lease for prefetch promotions so the DRAM replica survives until the follow-up get().ignore_cooldownbypasses only the client-side throttle backoff — never a master-side gate: the shared in-flight cap, holder check, and dedup TTL all still apply). The get never waits on promotions: it proceeds to the SSD read immediately, and a promoted replica benefits the next get.Default configuration is a complete no-op:
enable_ssd_prefetch=false.Relationship to prior work
ExistOptionsstruct rather than a bare bool) follows @LujhCoconut's suggestion on that RFC and theReplicateConfigpattern.existmust not promote) wired intoscripts/ci/run_ssd_offload_smoke.shwith its own master instance (promotion_on_hit=falseso the control is meaningful), and a reproducible A/B benchmark methodology.Measured impact
DSv4-Flash + vllm-ascend on Ascend A2; 48 long prefixes (at the prefix-cache hit granularity); fill → overflow-to-SSD → settle → measure;
promotion_on_hit=false; same dataset and seed across rounds. Absolute latency values are omitted; numbers are TTFT decrease relative to the offload-only baseline of the same round, computed as (baseline − prefetch) / baseline.Concurrency 2 (store DRAM comfortably sized):
Concurrency 4 (deliberately DRAM-constrained store, ~97.5% full — a
stress regime where eviction churns constantly):
NVIDIA H20:
Why the rounds differ so much: without prefetch, a queued request pays serial SSD reads on the critical path, so the offload-only baseline's TTFT degrades roughly linearly with queue depth, while the prefetch round hides those reads inside the queueing window and stays flat. The decrease scales with queue depth and with the SSD-read share of TTFT; an idle, serial workload sees ≈ 0 decrease by design.
Honest stress metric from the DRAM-constrained run: promotion completed 503, failed 860 (DRAM saturation), and the memory-pressure cooldown correctly yielded ~2.1k times — prefetch never starves eviction; it just loses.
Get-side decomposition (measured; drives one removal)
Earlier revisions also waited on in-flight promotions from the get path (
ssd_get_wait_ms). Decomposition measurements — prefetch+kick without waiting vs with waiting (a one-line decoupled build, since the knob previously gated both) — showed the wait never turned a pending get into a DRAM read and added latency instead, so it was removed and the get path returned to the upstream structure plus the kick. Absolute latencies are omitted as above; percentages are relative within each measured pair:The A/B numbers above were measured on a revision that included the wait; its isolated effect was negative in both tiers, so removing it does not degrade the reported gains. Zero
LEASE_EXPIREDon every arm of the decomposition runs.Design
Full component-level design with diagrams:
docs/source/design/store/ssd-prefetch.md(in this PR) and design RFC #4271.Commit-by-commit guide
[Store] Add read-only replica list queries on the client— registers the pre-existing admin read-only RPCs and adds client wrappers (no lease / no promotion / no metrics).[Bugfix][Store] Recognize mounted segments in IsReplicaOnLocalMemory— independent local-memory detection fix (self-contained, could land alone).[Store] Add PrefetchThrottle for best-effort SSD prefetch— sharded dedup state machine + cooldown; pure utility, unit-tested.[Store] Add RegisterPrefetchTask master RPC and prefetch lease— master side: tenant-aware, holder-checked, quota-shared, distinct idempotency,from_prefetchlease. Unit-tested.[Store] Add FileStorage::PrefetchKeys sharing the promotion execution chain— extractsPromoteOneKeyFromLocalDisk(promotion-on-hit behavior unchanged; its per-key loop body moves verbatim into the helper) and adds the best-effort batch driver.[Store] Wire SSD prefetch into exist/get paths— SsdPrefetcher, exist trigger, get-side demand kick, holder RPC. (Updated after review: the get-side wait that this commit originally introduced was measured and removed — see "Get-side decomposition" below.)[Store] Expose ExistOptions in C and Python bindings— store_c, store_py (keyword args), DummyClient (accepted, ignored with a log).[Store][CI] Add prefetch-on-exist integration test— Python integration test wired intoscripts/ci/run_ssd_offload_smoke.sh.[Docs][Store] Expand SSD prefetch design doc with component-level design— full component-level design with diagrams (docs/source/design/store/ssd-prefetch.md).[Docs][Store] Fix mermaid syntax in SSD prefetch design diagrams— diagram rendering fixes, docs only.[Store] Apply clang-format and ruff-format to prefetch changes— mechanical formatting, no behavior change.Merge branch 'main'— adapt to upstream [Store] Extract per-object metadata and runtime state headers from MasterService #4127/[Store] Centralize MasterService test access in a test-only peer #4135 (header split, MasterServiceTestPeer), [Store] Add ProbeKey/BatchProbeKey RPCs for lease-free existence probes #3801 (probeKey coexistence), [Store] Migrate lightweight Python utilities to unified package #3586 and [Doc] Group design documents by module in the docs sidebar #4196 (design doc moved todocs/source/design/store/ssd-prefetch.md); no feature changes.Rolling-upgrade compatibility
RegisterPrefetchTask,prefetch_offload_object) are additive: old masters/peers reject them and prefetch degrades to a no-op.enable_ssd_prefetch=false.How Has This Been Tested?
Test commands:
Manual end-to-end: A/B TTFT benchmark on vllm-ascend (fill → overflow → settle → measure; see "Measured impact").
Test results:
test_prefetch_on_existincl. negative control: plain exist does not promote)Checklist
./scripts/code_format.shdocs/source/design/store/ssd-prefetch.md)Reviews welcome from @LujhCoconut @huangtingwei9988 @Pz1116 @huangdong2022 — the design RFC (#4271) has the component-level details, and #3417 has the alignment discussion.
AI Assistance Disclosure