Skip to content

[Store] Prefetch SSD-only objects to DRAM on exist - #4272

Merged
fcczzz merged 50 commits into
kvcache-ai:mainfrom
DHX98:feat/ssd-prefetch-on-exist
Oct 26, 2026
Merged

fcczzz merged 50 commits into
kvcache-ai:mainfrom
DHX98:feat/ssd-prefetch-on-exist

Conversation

@DHX98

@DHX98 DHX98 commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor

Title: [Store] Prefetch SSD-only objects to DRAM on exist

Description

With SSD offload enabled, a key whose MEMORY replica has been evicted keeps serving reads from a LOCAL_DISK replica — paid synchronously on the get() critical path. For workloads that probe with exist before 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_exist with ExistOptions.prefetch_to_memory=true starts an asynchronous SSD→DRAM promotion for keys that turn out SSD-only, so the follow-up get() 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:

  • The exist path does a per-key dedup lookup and a thread-pool enqueue only — no RPC added to the probe (a sharded PrefetchThrottle with TTL dedup and a DRAM-saturation cooldown).
  • A bounded pool (4 threads) classifies candidates via read-only admin replica queries (no lease, no promotion-on-hit, no metrics side effects), then promotes local keys via a new RegisterPrefetchTask master 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 additive prefetch_offload_object RPC.
  • RegisterPrefetchTask is tenant-aware, holder-checked (holder_id == client_id), shares the promotion_in_flight_ cap and the per-key promotion_tasks entry with promotion-on-hit (double promotion is impossible), and reports PROMOTION_ALREADY_EXISTS distinctly so best-effort callers skip quietly. NotifyPromotionSuccess grants the usual read lease for prefetch promotions so the DRAM replica survives until the follow-up get().
  • Any get (single or batched) headed for SSD fires one demand-side promotion attempt for its disk keys (ignore_cooldown bypasses 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

  • RFC: [RFC]: Prefetch SSD-Only Objects to DRAM on Exist #2213 (@Pz1116). The API shape (ExistOptions struct rather than a bare bool) follows @LujhCoconut's suggestion on that RFC and the ReplicateConfig pattern.
  • This PR builds on the design pioneered by [Feature]Prefetch SSD-Only Objects to DRAM on Exist #2646 (@huangdong2022) and presents a new implementation on current main, shaped by the community's feedback on that PR (core commits credit huangdong2022 as co-author). Beyond a clean rebase, this version:
    • shares the promotion execution chain with promotion-on-hit instead of duplicating it (one chain, one failure-handling posture);
    • makes zero wire-protocol changes to existing RPCs (read-only queries ride the pre-existing admin RPCs; new RPCs are purely additive);
    • addresses the review findings on [Feature]Prefetch SSD-Only Objects to DRAM on Exist #2646 (throttle entries expiring while in-flight; per-key get-side master polling; missing completion callbacks; tenant handling in task registration);
    • ships a test plan that covers the feature end to end: unit tests for the throttle state machine and the master RPC semantics, a Python integration test with a negative control (plain exist must not promote) wired into scripts/ci/run_ssd_offload_smoke.sh with its own master instance (promotion_on_hit=false so the control is meaningful), and a reproducible A/B benchmark methodology.
  • RFC [RFC]: Explicit SSD→DRAM Prefetch Trigger for Mooncake Store #3417 (@LujhCoconut): see the alignment discussion there. This PR's mechanism layer (task registration, shared chain, dedup, lease, holder-local execution) is what [RFC]: Explicit SSD→DRAM Prefetch Trigger for Mooncake Store #3417's router-side explicit trigger would also build on; the exist-probe trigger is the RFC's "engine SDK without a router" path. We deliberately skip the frequency sketch (an explicit probe is the hotness signal) and the watermark gate in favor of a DRAM-saturation cooldown that yields to eviction/offload; promotion_in_flight quota is shared, not bypassed. A dedicated prefetch buffer / soft partition is left as follow-up.

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):

metric TTFT decrease with prefetch
median 5.2%
mean 5.1%
p99 22.0%

Concurrency 4 (deliberately DRAM-constrained store, ~97.5% full — a
stress regime where eviction churns constantly):

metric TTFT decrease with prefetch
median 85.2%
mean 81.4%
p99 44.9%

NVIDIA H20:

metric TTFT decrease with prefetch
median 27.4%
mean 6.7%
p99 0.3%

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:

comparison setup result
wait's isolated effect store get median, 15 trials +13% latency with wait (worse)
wait's isolated effect engine TTFT median, c=4, DRAM-constrained +28% TTFT with wait (worse)
kick vs promotion-on-hit threshold=2, 15 trials kick promotes on the first touch in 12/15 trials and puts the second touch at DRAM speed; promotion-on-hit does not promote within three touches in 13/15
kept config vs baseline engine TTFT median, c=4, DRAM-constrained −20% TTFT vs offload-only

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_EXPIRED on 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

  1. [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).
  2. [Bugfix][Store] Recognize mounted segments in IsReplicaOnLocalMemory — independent local-memory detection fix (self-contained, could land alone).
  3. [Store] Add PrefetchThrottle for best-effort SSD prefetch — sharded dedup state machine + cooldown; pure utility, unit-tested.
  4. [Store] Add RegisterPrefetchTask master RPC and prefetch lease — master side: tenant-aware, holder-checked, quota-shared, distinct idempotency, from_prefetch lease. Unit-tested.
  5. [Store] Add FileStorage::PrefetchKeys sharing the promotion execution chain — extracts PromoteOneKeyFromLocalDisk (promotion-on-hit behavior unchanged; its per-key loop body moves verbatim into the helper) and adds the best-effort batch driver.
  6. [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.)
  7. [Store] Expose ExistOptions in C and Python bindings — store_c, store_py (keyword args), DummyClient (accepted, ignored with a log).
  8. [Store][CI] Add prefetch-on-exist integration test — Python integration test wired into scripts/ci/run_ssd_offload_smoke.sh.
  9. [Docs][Store] Expand SSD prefetch design doc with component-level design — full component-level design with diagrams (docs/source/design/store/ssd-prefetch.md).
  10. [Docs][Store] Fix mermaid syntax in SSD prefetch design diagrams — diagram rendering fixes, docs only.
  11. [Store] Apply clang-format and ruff-format to prefetch changes — mechanical formatting, no behavior change.
  12. 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 to docs/source/design/store/ssd-prefetch.md); no feature changes.

Rolling-upgrade compatibility

  • No existing RPC signature changes.
  • New RPCs (RegisterPrefetchTask, prefetch_offload_object) are additive: old masters/peers reject them and prefetch degrades to a no-op.
  • Read-only queries reuse the pre-existing admin RPC handlers.
  • All knobs default off: enable_ssd_prefetch=false.

How Has This Been Tested?

Test commands:

ctest --test-dir build --output-on-failure -R \
  'prefetch_throttle_test|prefetch_task_master_test|client_readonly_query_test|promotion_on_hit_test|file_storage_promotion_test'
bash scripts/ci/run_ssd_offload_smoke.sh   # includes test_prefetch_on_exist.py

Manual end-to-end: A/B TTFT benchmark on vllm-ascend (fill → overflow → settle → measure; see "Measured impact").

Test results:

  • Unit tests pass (new: prefetch_throttle_test 11 cases, prefetch_task_master_test 5 cases, client_readonly_query_test 4 cases)
  • Integration tests pass (test_prefetch_on_exist incl. negative control: plain exist does not promote)
  • Manual testing: A/B TTFT above; promotion-on-hit regression tests green (shared execution chain)

Checklist

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

  • AI tools were used. The human submitter has reviewed and can defend every changed line.

DHX98 and others added 11 commits September 20, 2026 14:46
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.
@DHX98
DHX98 force-pushed the feat/ssd-prefetch-on-exist branch from fd36ce5 to f4e20d9 Compare September 28, 2026 03:08
@LujhCoconut

LujhCoconut commented Sep 28, 2026 •

Copy link
Copy Markdown
Collaborator

Overall Comment

The 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. PromotionAllocStart never evicts, so promotions only succeed on truly free DRAM;when the hot set is much larger than DRAM (plausibly the common case, since caches usually run full), successful promotions evict each other (thrash) while failed attempts keep costing master-side write-lock RPCs — and the only guard is a fixed-length backoff (5s by default) with no success-rate awareness. The benchmarks cover the favorable regime (48 long prefixes), not the adversarial one where the hot set exceeds DRAM.

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 comments

Inline 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 PrefetchKeys.

1. mooncake-store/src/ssd_prefetcher.cpp:366 — Dead branch: every client-visible replica query filters out non-COMPLETE replicas (TryGetReadableReplicaDescriptor, master_service.cpp:4166), so a PROCESSING memory replica can never be observed here and promotion_in_flight is always false. ssd_get_wait_ms thus only works for promotions started by this process; remote / other-process / on-hit promotions are invisible, contrary to the design doc. Either expose PROCESSING replicas via an admin variant or drop the branch and fix the docs.

2. mooncake-store/src/master_service.cpp:9762 — Two issues: (a) hardcoding execution_failures = 0 resets the kMaxPromotionExecutionFailures circuit breaker that TryPushPromotionQueue otherwise propagates (master_service.cpp:9650), so a persistently failing hot key can loop prefetch → fail → on-hit retry ×3 → prefetch forever when promotion_on_hit=true; (b) NotifyPromotionFailure re-records an on-hit candidate unconditionally (master_service.cpp:10129), and with promotion_on_hit=false the only sweeper early-returns (master_service.cpp:8759), so each prefetch failure permanently leaks a candidate entry (and pins the tenant state, since TenantState::Empty() includes candidates). Suggest skipping candidate re-record for from_prefetch and propagating the existing failure count.

3. mooncake-store/src/file_storage.cpp:1000 — PrefetchKeys should break on the first NO_AVAILABLE_HANDLE: today a saturated 128-key chunk burns ~384 doomed write-lock RPCs (register + alloc + notify per key) before the cooldown engages. Early-exit (releasing registered tasks) then enter cooldown.

Non-blocking

4. mooncake-store/src/real_client.cpp:7414 — [Non-blocking] ignore_cooldown=true bypasses the memory-pressure backoff exactly in the regime it exists for. Per-key dedup bounds the rate, but please document the interaction and consider a global cap on kick-driven attempts (e.g. after N consecutive failures, kicks honor the cooldown too).

5. mooncake-store/include/client_metric.h:561 — [Non-blocking] These counters are write-only: not ylt metrics, not in serialize(), nothing reads them. Wire them into the metrics output or drop them.

6. mooncake-store/include/prefetch_throttle.h:286 — [Non-blocking] With dedup_ttl=0 MaybeSweep never runs, so every distinct key accumulates forever. Skip inserting entries when dedup is disabled, or cap the table.

7. mooncake-store/src/file_storage.cpp:949 — [Non-blocking] Promotion-on-hit already stages through client_buffer_allocator_, but serially (at most one task per heartbeat on the single heartbeat thread); prefetch adds 4 concurrent workers drawing from the same pool that demand SSD reads (LoadBatch) depend on, so with large objects the staging allocations can starve demand gets (BUFFER_OVERFLOW). Worth a prefetch-specific share cap, or at least a doc note.

8. docs/source/design/store/ssd-prefetch.md:116 — [Minor] scripts/ssd-prefetch-ttft-ab/ is not in this PR

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).
@DHX98
DHX98 force-pushed the feat/ssd-prefetch-on-exist branch from 730b06e to 156293a Compare October 20, 2026 05:36
@fcczzz

fcczzz commented Oct 20, 2026

Copy link
Copy Markdown
Collaborator

@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.
@DHX98
DHX98 force-pushed the feat/ssd-prefetch-on-exist branch from 02a25e4 to 46ae5ea Compare October 23, 2026 12:24
…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.
@DHX98
DHX98 force-pushed the feat/ssd-prefetch-on-exist branch from 46ae5ea to 3418124 Compare October 25, 2026 01:32
@fcczzz
fcczzz enabled auto-merge (squash) October 26, 2026 02:35
@fcczzz
fcczzz disabled auto-merge October 26, 2026 02:38
@fcczzz
fcczzz merged commit f751cff into kvcache-ai:main Oct 26, 2026
31 checks passed
Juhyun-Kim-Memphis added a commit to Juhyun-Kim-Memphis/Mooncake that referenced this pull request Oct 26, 2026
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.
weijinqian0 pushed a commit to vllm-project/vllm-ascend that referenced this pull request Oct 28, 2026
### 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants