mirror of
https://github.com/kvcache-ai/Mooncake.git
synced 2026-11-05 01:17:55 +00:00
[GH-ISSUE #2430] [Performance]: segment utilization imbalance with heterogeneous segment sizes #644
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#644
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 @00fish0 on GitHub (Jun 11, 2026).
Original GitHub issue: https://github.com/kvcache-ai/Mooncake/issues/2430
Originally assigned to: @00fish0 on GitHub.
Describe your performance question
Summary
When mooncake-store is deployed with segments of different sizes, the larger segments consistently show lower utilization than the smaller ones. After digging into the allocation and eviction code, we believe this is structural: allocation is capacity-blind (uniform per segment), and eviction is segment-blind (global watermark + global LRU-by-lease). Neither side has a feedback mechanism to balance utilization across heterogeneous segments.
We don't have a solution we're fully happy with (see "What we considered" below — including
free_ratio_first, which helps but has its own issues), so we'd like to open this up for discussion.Our deployment & observation
Mooncake Version: v0.3.9
10 memory segments across 5 nodes, two different sizes (node IPs anonymized):
Global: 4.56 TiB / 5.08 TiB (90%). Workload is an LLM prefix-KV-cache pool (SGLang HiCache L3 backend), default
randomallocation strategy, eviction parameters (eviction_high_watermark_ratio=0.9,eviction_ratio=0.05).The 1000 GiB segments are always ~5 percentage points below the 400 GiB ones, not just transiently. Note also that the eight 400 GiB segments sit at a suspiciously tight 91.6% — this appears to be their practical fill ceiling (allocation failures from fragmentation), not a coincidence of timing.
Why this happens (code analysis)
1. Allocation is capacity-blind
RandomAllocationStrategy::Allocate(mooncake-store/include/allocation_strategy.h) picks a segment uniformly at random over segment names, regardless of capacity or free space. A 1000 GiB segment gets exactly the same write inflow as a 400 GiB one. The only way a large segment absorbs more data is spillover: a random pick lands on a full segment, fails, and the retry loop walks to the next segment that still has space.The steady-state dynamics that follow: small segments fill up and pin at their ceiling; large segments spend most of the time "catching up" via spillover, so any snapshot shows them at lower utilization.
2. Eviction is segment-blind
MasterService::BatchEvict(mooncake-store/src/master_service.cpp) evicts by lease age across all metadata shards, with no awareness of which segment a replica lives on. There is no per-segment watermark and no way to relieve pressure on a specific segment. The trigger is also global-only:global_used_ratio > eviction_high_watermark_ratio;need_eviction_, which is only set after aPutStartfails to allocate on every segment.3. The two interact badly
With our topology the small segments cap out at ~91.6%, so the global used ratio mathematically cannot reach the default 0.95 watermark (
(8×366 GiB + 2×1000 GiB) / 5200 GiB ≈ 94.8%). The watermark path therefore never fires, and eviction is driven entirely by the allocation-failure fallback. Each cycle ends with a window of client-visibleNO_AVAILABLE_HANDLEwrite failures, followed by a bulk eviction of ~5% of all objects (~250 GiB of cache dropped at once in our case), then refill — a sawtooth in both write success rate and hit rate.Why this matters
What we considered
FreeRatioFirstAllocationStrategy(already in the codebase): sorts sampled candidates by free ratio and greedily picks the emptiest. This does equalize utilization percentages in steady state, but the greedy choice introduces a new problem: the emptiest segment wins almost every allocation until it catches up. A freshly (re)mounted segment — e.g. after a node restart — has free ratio 1.0 and absorbs nearly the entire cluster write bandwidth until it reaches parity, which is a single-node write storm on exactly the node that just came back. The strategy comment frames "new empty segments naturally win" as a feature, but for large segments this is a long, severe transient. It also does nothing for the eviction side (the unreachable watermark and segment-blind eviction remain).eviction_high_watermark_ratio/eviction_ratioso the watermark path fires before allocation failures and evicts in smaller steps. This mitigates the sawtooth but requires knowing each deployment's fragmentation ceiling, and still does nothing for the utilization/traffic imbalance.Possible directions (for discussion)
None of these is obviously right; we'd love input from the community:
free_ratio_first.BatchEvictprefer evicting replicas on pressured segments (the candidate selection currently ignores placement entirely).max(per-segment ratio)instead of the global average, so eviction doesn't degenerate to the allocation-failure path.Before submitting a new issue...
@github-actions[bot] commented on GitHub (Jun 11, 2026):
Thanks for opening this issue, @00fish0!
50252876A maintainer will triage this when possible. To help us respond faster, please include:
Useful links: Documentation · Contributing guide
@wdlin233 commented on GitHub (Jun 30, 2026):
Hi @00fish0, thanks for the thorough analysis in this issue — it was really helpful for understanding the problem space.
I've been investigating possible approaches and ended up implementing a capacity-aware power-of-two-choices (P2C) allocation strategy as a first step. The code is on the branch issue-2430 on my fork if you want to take a look.
What it does
For each allocation,
CapacityAwareP2CAllocationStrategy:Preferred-segment and excluded-segment behavior is identical to the existing strategies; replicas of the same slice still land on distinct segments.
Why this approach
We need three things at once:
(a) balanced utilization across heterogeneous segments,
(b) bounded write storm on fresh-segment mount,
(c) capacity-proportional inflow in steady state.
Randomignores capacity entirely. Fails (a) and (c).FreeRatioFirstnails (a) and (c) but an empty mounted segment wins every selection. Fails (b).free_bytesorcapacity * free_ratioas weights) and would be sampled with that probability. Same storm problem, different mechanism.P2C samples uniformly (each segment gets at most 2/N selection probability) but makes the choice capacity-aware in the comparator. The
2/Nbound is what limits the late-mount storm; the free_ratio → free_bytes (primary key → tiebreaker) comparator is what routes capacity-proportional inflow.Quantitative results
I ran two scenarios against
RandomandFreeRatioFirst. Numbers are fromallocation_strategy_bench(and a standalone test on the issue topology, but running single-node with in-process allocators. This validates the algorithm-level behavior but does not yet exercise multi-node RDMA paths or real KV-cache workloads).Scenario A — issue #2430 topology, steady state
8 segments × 8 MiB + 2 segments × 20 MiB (2.5x ratio, matching the issue's 400 GiB / 1000 GiB ratio), driven to 70% cluster utilization.
Random: large segments received only 20.5% of writes despite owning 38.5% of capacity, and per-segment utilization spans 57 pp — exactly the pathology you reported.CapacityAwareP2Cachieves 37.8% large-share (0.7 pp below capacity share, vs. Random's 18 pp gap) and a 2.1 pp utilization spread (vs. Random's 57.1 pp). It was nearly indistinguishable fromFreeRatioFirstin steady state.Scenario B — late-mount write storm
8 segments pre-heated to ~70%, then a 9th fresh segment is mounted (
free_ratio = 1.0). Of the next 500 allocations, what fraction lands on the fresh segment? Uniform-random baseline = 1/9 ≈ 11%.FreeRatioFirstsends 67% of writes to the freshly-mounted node, butCapacityAwareP2Csends 20% which closes to the theoretical2/N ≈ 22%bound, with a small uplift from the bytes-tiebreak. ~3.3× storm reduction.Scenario C — in-tree scaleout benchmark (100 segments, 512 KiB)
The existing
--workload=scaleoutmatrix at 100 segments, 2000 allocations, 20% post-scaleout allocation:P2C has the lowest utilization spread, highest final utilization, and 100% allocation success (vs. 92% and 97% for the other two). Throughput is also highest — fewer fallback retries and fewer failures more than compensate for the small per-allocation overhead.
Summary
CapacityAwareP2Cis the only strategy currently in the tree that does well on all three.What this does NOT address
This PR only tackles the allocation side. With P2C, multiple segments may reach their fragmentation ceiling around the same time, which could cause a coordinated eviction spike (sawtooth). If that shows up in practice, a per-segment eviction watermark — triggering BatchEvict when any single segment crosses the threshold rather than waiting for the global average — would be a natural follow-up.
Here's the approach I've implemented so far, along with my reasoning. Would love to hear your thoughts on it.
@github-actions[bot] commented on GitHub (Sep 29, 2026):
This issue has had no activity for 90 days and will be closed in 7 days if there is no further activity. Please comment or react if it should stay open.
@github-actions[bot] commented on GitHub (Oct 7, 2026):
Closing due to 3 months of inactivity. If this is still relevant, please comment and we can reopen.