mirror of
https://github.com/kvcache-ai/Mooncake.git
synced 2026-11-05 01:17:55 +00:00
[GH-ISSUE #3528] [Bug&RFC] Bucket LOCAL_DISK: clients sharing a storage_path collide on bucket ids → L3 cache broken #903
Labels
No labels
ascend
auto-closed
bug
bug
CI/Build
Common
documentation
enhancement
feature request
good first issue
help wanted
high priority
MetaX
nightly-failure
pull-request
PyTorch Backend
question
RFC
Roadmap
stale
Store
Tests
Transfer Engine
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
kvcache-ai/Mooncake#903
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Originally created by @Morpheus799 on GitHub (Aug 19, 2026).
Original GitHub issue: https://github.com/kvcache-ai/Mooncake/issues/3528
Bug Report
Summary. When multiple clients share one
storage_path(e.g. SGLang embeds one Mooncake client per TP rank on the samessd_offload_path), the bucketLOCAL_DISKbackend has no per-client namespacing and no stable identity. This causes three coupled problems: silent collision, ambiguous recovery ownership, and per-client accounting that can neither be attributed nor survive restart. Existing issues cover pieces; none covers the "N clients, onestorage_path" case.Environment. Mooncake bucket
LOCAL_DISKbackend (main). Occurs whenever multiple clients share onestorage_path— e.g. SGLang embeds one Mooncake client per TP rank on the samessd_offload_path(sgl-project/sglang#31926).Three coupled problems (
mooncake-store/src/storage_backend.cppunless noted):BucketIdGeneratorseeds(time_gen() << 12) | 0(whole-second) then onlyfetch_adds; filename<storage_path>/<bucket_id>.buckethas no client tag; openedO_CREAT|O_TRUNC, noO_EXCL. Same-second construction → identical ids → silent overwrite. The backend struct holds onlystorage_path_— noclient_id/pid to disambiguate.Init()scans the whole dir and adopts every.meta, keyed by numericbucket_id;MasterService::AddReplica(master_service.cpp) keeps one LOCAL_DISK replica per key with noclient_idcheck (see #3052). Shared dir → every client claims every file → arbitrary first-claim → dangling refs when the "owner" evicts.client_idis a random UUID minted perClientctor (client_service.cpp, boost random), not stable across restart, and never assigned by the master. So a client cannot be recognized as the same logical owner after a restart, and its on-disk data cannot be attributed to it — which is what makes per-client, layout-independent accounting impossible today.Related. #3052 (client-restart /
client_idvalidation), #2306 + #2777 (marker re-adoption — marker is per-directory), #3030 (replica-id validation), #3220 + #3221 (per-object delete + GC), #2721 (SSD-capacity report), SGLang sgl-project/sglang#31926.The gap. All of the above assume one client per
storage_path. "N clients, one directory" is unsupported, and even the identity primitives that exist (RFC #2306's marker) are per-directory.Proposal (we prefer B; A is a stopgap):
owner_idin each bucket's.meta(not the filename or directory). Per-client accounting andInit()recovery then filter byowner_id, so both work for any layout — shared dir, per-client dirs, or multiple disks. Master re-adopts by matching the in-metadataowner_id(not #2306's per-directory marker). Per-client bytes are reported to the master (cf. #2721); a node/disk cap is enforced by master-side aggregation, not a shared-dir scan.owner_id— a master-side registry keyed by a stable client handle, or a minimal persisted id decoupled from the offload directory.Ask. Decide the ownership model for a shared
storage_path. We advocate (B) so per-client identity and accounting become layout-independent; (A)/#31926 is a stopgap. This should be coordinated with the warm re-adoption work (#2306). Willing to contribute the PR once the direction is agreed.Before submitting...
@Morpheus799 commented on GitHub (Aug 19, 2026):
Independent reproduction. We reproduced this on DeepSeek-V4-Flash (MLA), TP8, 8 ranks per pod sharing one ssd_offload_path, on the current bucket backend. The collision precondition is deterministic: all 8 ranks in a pod seed their BucketIdGenerator to the identical value (observed 7320020267008 ×8; a separate earlier run showed 7319298519040 ×8) — time_gen() is whole-second, the TP ranks initialize within the same second, and NextId() never re-reads time, so the sequences stay identical for the whole run. Under load this surfaces as read failures carrying the truncation fingerprint storage_backend.cpp: Read size mismatch for key ..., expected: N, got: 0 (a bucket that existed and was O_TRUNC-overwritten by a colliding rank), and these appear before any eviction runs — so they are the collision itself, not an eviction/read race. Requests still succeed because HiCache treats the failed read as a miss and recomputes; the corruption is therefore silent at the API level, but the L3 cache is effectively unusable under this configuration.
@github-actions[bot] commented on GitHub (Aug 19, 2026):
Thanks for opening this issue, @Morpheus799!
92247184A maintainer will triage this when possible. To help us respond faster, please include:
Useful links: Documentation · Contributing guide
@LujhCoconut commented on GitHub (Aug 21, 2026):
Thanks for the thorough report. I checked the code paths on
mainand agree the problem is real: the(time_gen() << 12)seed carries no client tag, bucket files open withO_CREAT|O_TRUNC(noO_EXCL), andInit()adopts every.metain the directory.From my understanding, the recommended way to run SSD offload today is standalone mode: start a dedicated store service to own the KV-cache pool (and the SSD tier), with the inference ranks as pure requesters. That keeps one client per
storage_path, so this class of collision doesn't arise.I'm not familiar with SGLang's code, but on the vLLM side there's an extra reason this fits naturally. vLLM dedups TP-rank keys before they reach the store: each cache group's key namespace is
tp_rank // tp_replication_factor(_spec_tp_replication_factorinworker.py—tp_sizefor pure MLA,max(1, tp_size // num_kv_head)for GQA), so all TP ranks holding identical KV collapse into the same key namespace,put_step(the same factor) stripes their PUTs across that one shared key space, and abatch_is_existcheck dedups before every put. In other words, the logical cache is already "one shared pool" — standalone mode just gives that shared pool a single concrete owner, which is exactly the shape the dedup assumes. Running N embedded clients against one shared offload dir works against that shape.Happy to help push the fix (fail-fast / owner enforcement) along.
@rishabhsinha17 commented on GitHub (Aug 25, 2026):
+1 on (B), with a concrete shape for the open design point (the durable anchor), from the SGLang embedded-client side.
Standalone mode avoids the collision, but the embedded shape is real and staying: SGLang embeds one client per scheduler rank, and per sgl-project/sglang#31926 dp-attention reports the attention-group-local tp_rank (0 on every DP rank when attn_tp_size == 1), so "one client per
storage_path" cannot be assumed by construction. The per-rank subdir there is the right stopgap, but it keeps identity bound to directory layout, which is exactly what (B) removes.Anchor proposal: let the client pass an optional stable logical handle at construction (SGLang would pass
engine_id:dp:tp:pp; no handle keeps today's random UUID and behavior). The master persists a handle -> owner_id mapping and returns the owner_id at registration; the backend stamps it into every.meta.Init()adoption and byte accounting then filter by owner_id, layout-independent, and #3052 gets a stable identity to validate restarts against.Migration: version the
.meta. Legacy metas (no owner_id) are adopted only when the directory is detected single-owner; in a shared dir they are quarantined for GC instead of first-claim adopted. Independent hardening worth landing under either option: salt theBucketIdGeneratorseed with the client id, and open bucket files withO_EXCLplus retry-on-collision; today the seed is whole-second (storage_backend.cpp:1689),NextId()never re-reads time, and the data file opensO_CREAT|O_TRUNC(line 3912), so same-second clients collide for the entire run and the loser is silently truncated.Guard for existing deployments: on
Init(), take a lock/marker instorage_pathand fail fast if another live client owns it, so today's shared-dir setups fail loudly at startup instead of corrupting L3.Happy to implement once the direction is agreed: the owner_id path including the guard, or the guard plus id hardening alone as a first PR.
@quantz8a commented on GitHub (Sep 24, 2026):
Fixed by #4172 (merged
c30f983). LOCAL_DISK now fail-fasts when a second live client sharesstorage_path(.mooncake_local_disk.lock+ docs for per-rank dirs).Closing as completed.