mirror of
https://github.com/kvcache-ai/Mooncake.git
synced 2026-11-05 01:17:55 +00:00
[GH-ISSUE #4207] [RFC]: Export process and allocator memory metrics, and complete the jemalloc build option #1056
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#1056
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 @DeyunYang on GitHub (Sep 18, 2026).
Original GitHub issue: https://github.com/kvcache-ai/Mooncake/issues/4207
Summary
Mooncake exports no process memory metrics today. There is no
mooncake_process_rss_bytes,nothing sourced from
/proc/self/status, and no allocator visibility of any kind. When amaster's RSS climbs, the only available tools are a code audit and a heap profiler attached
after the fact.
This RFC proposes adding a process/allocator metric surface to both the master and the
client, and finishing the
STORE_USE_JEMALLOCoption introduced in #902 so that theallocator it selects can actually be observed.
STORE_USE_JEMALLOCstays OFF by default. This is not a proposal to change the default.Motivation
Two threads converge here.
#3452 (mooncake-master OOM under a long-context PD-separation workload) was closed as
fixed on main, but the discussion is instructive: diagnosing it took a structure-level audit
of the
MasterServicemetadata lifecycle because no metric could distinguish "the allocatoris holding freed pages" from "Mooncake is leaking". The closing analysis names the mechanism
explicitly — temporary allocations that glibc/jemalloc arenas retained after the workload
stopped. In that thread, @ykwd suggested:
That suggestion is hard to act on today: a release binary is stripped,
STORE_USE_JEMALLOCemits no runtime evidence that it took effect, and even with jemalloc linked there is no
series showing whether pages are being purged.
#1436 proposed defaulting jemalloc ON. It got an LGTM and stalled on a single concern —
jemalloc is not packaged on every distribution — then was auto-closed for inactivity. That
concern is specific to flipping the default and does not apply to this RFC.
Independently, we hit the same class of problem in production and traced it to the bottom.
The measurements are in Production evidence below; they are what the metric surface
proposed here was built to make visible.
Production evidence
Master-side and client-side, all metadata-churn driven.
The restart tax, measured with syscall tracepoints
On a production leader holding ~264M keys, two things were happening. Every client restart
stepped master RSS up by roughly 15 GB and it never came back down, five times in a single
day. Underneath that, RSS also grew steadily through normal serving, with no restart
involved. Neither is fixed by an HA failover. The figure below is the one traced restart,
measured end to end.
We attached
sys_enter_mmap/mprotect/munmap/brk/madvisetracepoints filteredto the master pid across one controlled client restart, sampling
VmRSSand master metricsevery 15s. Overhead was negligible.
Three readings:
madviseand 0munmapcalls;RSS moved 42 MB in four minutes.
kernel for 14.9 GiB of fresh memory. Reuse was measured at 0, not "low". Grouping the new
commits by 64 MB alignment:
heaps=253 total=15.33 GiB min=1.3MB p50=62.5MB p90=64.0MB max=64.0MB— 253 brand-new 64 MB heaps filled one by one (253 x 64 MB ~ 16.2 GiB).mprotecttotal 15.33 GiB matches the 15.23 GiB RSS risealmost byte for byte, so there is no other consumer hiding in the number.
The mechanism is glibc's: it returns free memory only from the top of a heap, and the erased
metadata sat in the middle.
After switching the master and client to jemalloc
Same deployment,
MALLOC_CONF=background_thread:true, allocator linked in (verified via/proc/<pid>/maps, notLD_PRELOAD):24h continuous sampling (2-min interval, 718 samples, zero gaps,
restarts0 throughout, asingle incarnation):
Over the same window the resident set turned over completely — evictions +268M, puts +272M,
allocated_bytesswinging between 3.0 and 4.6 TB across a dozen fill/evict cycles — whilekey_countstayed within 267.90-268.01M. So the RSS decline is not "keys went away". Theper-2h troughs descend monotonically and then flatten (154.92 -> ... -> 152.39 -> 152.53 ->
152.69), the exact inverse of the glibc staircase.
A second deployment with sustained rather than restart-driven churn (483M evictions against a
41M resident key count, ~12x turnover) showed the same allocator behavior at ~750 B/key under
glibc.
The client was verified the same way, and that result is why this proposal links the allocator
into
mooncake_clientrather than the master alone. The client under test had SSD offloadenabled, which puts it in the same position as the master: every offloaded object carries a
metadata entry in the client's own maps —
object_bucket_map_, plus the LRU index and thepending-key sets beside it — so an offload workload allocates and frees small records at the
rate objects move, and glibc holds the freed pages exactly as it does on the master. On a
client serving a 900 GB segment, resident memory under glibc grew past 1000 GB and was still
climbing when the run was cut. With jemalloc linked, the same workload settles at around
930 GB and stops growing.
Why this argues for the metrics, not just for jemalloc
Every number above came from syscall tracing and
/procscraping done by hand, after thefact, on a live leader. None of it was visible in Mooncake's own metrics, and the two
hypotheses that mattered — "decay never ran" versus "decay ran but fragmentation pins the
pages" — produce identical curves under cgroup-level RSS. The series proposed here
(
dirty_purge_runs_totalanddirty_madvises_totalfor the first,resident/allocatedandactive/allocatedplus the per-size-class breakdown for the second) separate them directly,and
mooncake_process_rss_peak_bytescatches theVmHWMthat OOM decisions actually use andthat a 15s scrape otherwise misses.
Design
Metric surface
A single
AllocatorMetricclass, held by bothMasterMetricManagerandClientMetric, somaster and client expose the same series and a scrape can be compared across the two.
Process series, from
/proc/self/status, exported regardless of which allocator islinked:
mooncake_process_rss_bytes,..._rss_peak_bytes,..._rss_anon_bytes,..._rss_file_bytes,..._rss_shmem_bytes,..._vsize_bytes,..._swap_bytes.These are the ground truth the allocator series are read against.
mooncake_jemalloc_resident_bytesis jemalloc's own upper estimate over the extents it maps and can exceed the kernel's resident
size, so the two are compared for divergence rather than subtracted.
Allocator series, from
mallctl:mooncake_jemalloc_enabledplus allocated / active /metadata / resident / retained / mapped / dirty / muzzy bytes, arena and background-thread
counts, the
opt.dirty_decay_msandopt.muzzy_decay_msknobs, and cumulative purge counters(
dirty_purge_runs_total,dirty_madvises_total, …). Dirty and muzzy are kept apart ratherthan summed, because jemalloc 5 decays them on separate paths and exposes no combined counter.
Per-size-class occupancy:
mooncake_jemalloc_bin_regs,..._bin_slabs,..._bin_used_bytes,labelled by size class. A slab returns to the OS only once every region in it is free, so a
size class sitting at low occupancy is holding
slab_byteswhile usingused_bytes— this isthe series that localizes fragmentation to a specific allocation size.
Keeping libmooncake_store allocator-free
libmooncake_storeis also consumed by the Python extension, which must not have an allocatorforced on its host process. So:
mooncake_masterandmooncake_clientexecutables only.STORE_USE_JEMALLOCcompile definition is scoped to those two targets.AllocatorMetricnames no jemalloc type. It calls aJemallocStatsCollectorfunctionpointer installed at startup by a binary that does link the allocator
(
SetJemallocStatsCollector). With no collector installed,mooncake_jemalloc_enabledreports 0 and the process series still work — which is the correct answer for a process
running the system allocator, the Python extension included.
This also means the metric half of this change is useful with
STORE_USE_JEMALLOC=OFF, i.e.in the default build, and on distributions where jemalloc is unavailable.
Startup evidence
LogAllocatorStatus()logs the jemalloc version,background_threadstate, live backgroundthread count, and both decay knobs at startup. Release binaries are linked with
-s, so thisline is the only evidence that jemalloc actually replaced the system allocator;
mallctlresolves only when jemalloc is linked and glibc exposes no equivalent knob. It compiles to an
empty function when
STORE_USE_JEMALLOCis off.Failure behavior
Collection fails as a whole rather than partially. A build without
--enable-statsstillanswers the epoch write, so a partial snapshot would be indistinguishable on a dashboard from
a healthy one that happens to read zero. On failure
mooncake_jemalloc_enabledreports 0.Scrape rate limiting
Advancing the jemalloc epoch takes a lock shared with allocating threads, so
Refresh()reuses already-published values for samples closer together than a minimum interval, rather
than letting an unauthenticated
/metricsendpoint drive that rate.Build variable reset
#902's${JEMALLOC_STATIC_LIBRARIES}is kept as is. ItsMASTER_EXTRA_*indirection iskept too, renamed
ALLOCATOR_*because the client now uses it, and extended with thecompile definition and library directory.
That indirection is load-bearing rather than stylistic:
pkg_check_moduleswrites itsJEMALLOC_*results into the CMake cache, so referencing those names directly on thetargets leaves a build directory reconfigured back to
STORE_USE_JEMALLOC=OFFstilllinking the allocator while the compile definition is gone — the binary runs on jemalloc
while reporting
mooncake_jemalloc_enabled 0. The plainALLOCATOR_*variables arecleared on every configure, which the cached names cannot be.
Compatibility
STORE_USE_JEMALLOCremains OFF by default; no existing build changes behavior.hybrid_metric.hgains abasic_hybrid_gaugealongside the existingbasic_hybrid_counter, via a delegating constructor, so that the per-size-class series candeclare
# TYPE gaugeinstead of telling Prometheus a value that can fall is monotonic.Size
1118 added lines, 246 of them tests and 34 CMake. Filed as an RFC per
CONTRIBUTING.md's 500-LOCthreshold. Proposed as a single PR with two commits (link, then metrics): splitting it would
leave the metrics commit shipping a collector hook with no in-tree caller.
Testing
allocator_metric_testcovers: process series present without a collector; jemalloc serieszero and
enabled=0without a collector; full series with a collector installed; a failingcollector leaving
enabled=0rather than publishing zeros; cumulative counters trackingjemalloc's own totals rather than a sum of deltas; and per-bin series with size-class labels.
@github-actions[bot] commented on GitHub (Sep 18, 2026):
Thanks for opening this issue, @erlangx!
75969023A maintainer will triage this when possible. To help us respond faster, please include:
Useful links: Documentation · Contributing guide
@DeyunYang commented on GitHub (Sep 19, 2026):
Implemented in #4208