mirror of
https://github.com/kvcache-ai/Mooncake.git
synced 2026-11-05 01:17:55 +00:00
[GH-ISSUE #3639] [TE] Unconditional ibv_fork_init() makes every register/unregister cycle permanently split VMAs (MADV_DONTFORK never reverted) → vm.max_map_count exhaustion #929
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#929
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 @thincal on GitHub (Aug 24, 2026).
Original GitHub issue: https://github.com/kvcache-ai/Mooncake/issues/3639
Summary
RdmaContextunconditionally callsibv_fork_init()at engine startup (and the new TENT transport does the same). Once verbs fork protection is active, everyibv_reg_mrperformed through the transfer engine issuesmadvise(range, MADV_DONTFORK)on the registered range, and the correspondingunregisternever issuesMADV_DOFORK. The kernel therefore tags those pagesVM_DONTCOPY, which splits the containing VMA and makes it permanently unmergeable with its neighbors.For workloads that register/unregister buffers at high frequency (per-request buffer registration rather than a registered-once pool), this is a monotonic leak of VMA boundaries:
vm.max_map_countis eventually exhausted, after whichibv_reg_mrandibv_create_qpfail withENOMEM— silently, with nothing in dmesg — and the host application typically falls over.Impact (production incident)
A production multimodal serving deployment (SGLang, EPD disaggregated encoder→prefill transfer of per-request image/video embeddings, i.e. one register/transfer/unregister cycle per embedding) hit exactly this:
mooncake register_memory failed (ret=-202 ERR_CONTEXT), C++ sideFailed to register memory 0x...: Cannot allocate memory [12](rdma_context.cpp) andFailed to create QP: Cannot allocate memory(rdma_endpoint.cpp), followed by a crash loop.mlx5messages, noCREATE_MKEY no resources, no page-allocation failures. TheENOMEMoriginates in userspace:ibv_create_qpneeds to mmap doorbell/UAR pages andibv_reg_mr's backing allocation needs fresh mmaps, both of which fail oncevm.max_map_countis reached. This makes the failure very hard to attribute without knowing the mechanism.vm.max_map_count(65530 default → 1M) only delays the cliff (~days at our traffic rate); it does not stop the ratchet.Root cause
Unconditional fork protection, no switch:
v0.3.10.post1,mooncake-transfer-engine/src/transport/rdma_transport/rdma_context.cpp(RdmaContext ctor):Same in current main / v0.3.12.post1 TENT path:
mooncake-transfer-engine/tent/src/transport/rdma/context.cpp(~L298):int ret = verbs_.ibv_fork_init();ibv_fork_init()is process-global: once any engine context exists, everyibv_reg_mrin the process (mooncake's and any other verbs user's) madvises its rangeMADV_DONTFORK.Deregistration does not undo it.
RdmaContext::unregisterMemoryRegion()destroys the MR but never callsmadvise(addr, len, MADV_DOFORK).VM_DONTCOPYpersists; even after the memory is freed and recycled by the allocator, the VMA split boundaries stay, and the next registration at a different size/offset splits further — a one-way ratchet. (Sizes vary per request in real workloads; with fully static sizes/addresses it would plateau, which is why this escapes simple benchmarks.)Evidence collected on the affected host
strace -f -e trace=%memory -kon the encoder process: onemadvise(0x..., <registered-size>, MADV_DONTFORK)perengine.register()call, issued from the threads performing the registration./proc/<pid>/smaps: the split VMA families (exact sizes = the registered embedding sizes) carry thedcflag — e.g.VmFlags: rd wr mr mw me dc ac sd— on ~91k VMAs in one process; the remaining split pieces (recycled memory) keep their boundaries without the flag./proc/<pid>/mapsline count grows monotonically with traffic and never drops; the dominant entries are two exact-size families matching the registered buffer sizes (36,864 B and 147,456 B in our case, ~88k each after ~13h).Minimal reproduction
Watch the
/proc/self/mapsline count: it ratchets up with register/unregister cycles and never comes back down. The essential ingredients are register→unregister cycles of varying sizes on varying addresses; with constant sizes/addresses the split count plateaus, which is why simple benchmarks miss it.Suggested fixes
unregisterMemoryRegion(rdma_transport and the TENT path), callmadvise(addr, length, MADV_DOFORK)afteribv_dereg_mrso the kernel can clearVM_DONTCOPYand re-merge the split pieces. This keeps fork protection semantics for live MRs while making the churn reversible.MC_IB_FORK_PROTECTION=0/1): integrators that never fork (long-lived inference servers are the common case for the transfer engine) currently pay permanent address-space fragmentation with no way to decline.vm.max_map_countinteraction in the troubleshooting guide — today the doc lists raising it as a workaround for MKEY exhaustion, but the mechanism above is a distinct failure mode with the same two error strings (Failed to register memory ... Resource temporarily unavailable,Failed to create QP: Cannot allocate memory) and no firmware involvement.Workaround we adopted
Register-once buffer pools (never unregister): transfer out of a fixed set of pooled, permanently registered buffers. VMA count plateaus at the pool size instead of growing with traffic. (SGLang side: the EPD encoder sender now copies each embedding into a registered-once pool buffer, mirroring what the receiver already did.)
@github-actions[bot] commented on GitHub (Aug 24, 2026):
Thanks for opening this issue, @thincal!
1206509A maintainer will triage this when possible. To help us respond faster, please include:
Useful links: Documentation · Contributing guide
@thincal commented on GitHub (Aug 25, 2026):
And some other findings FYI:
Before calling
ibv_fork_init(), queryibv_is_fork_initialized()when the symbol is available.IBV_FORK_UNNEEDED, do not callibv_fork_init(). Fork protection is unnecessary on this verbs/kernel stack, and skipping it avoids theMADV_DONTFORKVMA-splitting path entirely.ibv_fork_init()as before.