mirror of
https://github.com/kvcache-ai/Mooncake.git
synced 2026-11-05 01:17:55 +00:00
[GH-ISSUE #4625] [Bug] Classic RDMA client (real_client.cpp): crash and silent write failures on peer-membership change #1174
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#1174
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 @sivabreddy on GitHub (Oct 13, 2026).
Original GitHub issue: https://github.com/kvcache-ai/Mooncake/issues/4625
Summary
Two distinct, reproducible problems in the "classic" RDMA client
(
real_client.cpp), both triggered by a change in who's in the client pool(a pod joining or leaving, by any method):
just the Mooncake connection.
Putcan fail withTRANSFER_FAILwith noerror surfaced to the caller — the data is never actually written to the
shared store, but nothing above the Transfer Engine layer is told this
happened.
Both are reproducible on the latest PyPI release at time of testing
(
mooncake-transfer-engine==0.3.13.post1, 2026-08-31). We believe both mayshare a root cause with the already-fixed #3595 (a stale/self-referential
peer reference surviving a membership change), but in the older/classic
transport, which does not appear to carry that fix.
Environment
mooncake-transfer-engine0.3.13.post1 (PyPI,installed via
uv pip install mooncake-transfer-engine, no sourcemodifications)
v0.4.1-gdfc914cba(
LMCacheConnectorV1), using Mooncake Store as LMCache's remote backend(
remote_url: mooncakestore://...) — not the vLLM-nativeMooncakeStoreConnectormaster + metadata server as separate lightweight pods in the same
namespace
GID index 3mooncake-config.yamlused throughout (templated;local_hostnameset toeach pod's own IP at container start):
Of the 8 listed devices, only
mlx5_0has a real network attachment in ourenvironment; the other 7 fail to open at startup with a clear, non-fatal
warning and are excluded from the data path. This is expected and not itself
the bug — it becomes relevant in one test below.
Issue 1: segfault on peer-membership change
What we did (1): confirm the mechanism works normally
Deployed 2 vLLM+Mooncake pods (Pod A, Pod B) on separate nodes. Pod A stored a
prefix. Pod B requested the same prefix.
What we found (1)
A genuine cross-pod RDMA retrieve, confirmed from LMCache's own log line on
Pod B (
Inference Engine computed tokens: 0— Pod B never computed thisitself — followed by a real
Retrieved N tokensline). No errors. This isthe expected, working behavior.
What we did (2): change pool membership, then send a real request
Changed which pods were in the pool, by each of these methods independently:
kubectl delete pod --grace-period=0 --force) andlet Kubernetes replace it.
kubectl delete pod, default grace period,clean SIGTERM) and let Kubernetes replace it.
After each change, sent a real inference request that would cause a Mooncake
GetorPut— to either the new/replacement pod or an existingpod that was never itself replaced.
What we found (2)
Whichever pod's turn it was to perform that next real operation segfaulted
— every time, regardless of which of the three membership-change methods was
used, and regardless of whether it was the new pod or an untouched existing
pod. This reproduced independently on 4 separate occasions. Exactly one
crash occurred per membership-change event. The affected process self-healed
after one container restart (10-15 seconds) and was stable again until the
next membership change elsewhere in the pool.
Crash log, one example (a replacement pod's first retrieve attempt):
A second example (a replacement pod's own client failing to initialize at
all, before any successful operation) shows the retry-then-crash shape more
fully:
What we did (3): test a candidate fix (MC_USE_TENT)
Found
MC_USE_TENTas a string inside the installedmooncake/engine.soviastrings, hypothesizing it would opt into the newer TENT RDMA transport,which already has a fix (#3598/#3603) for a structurally similar crash
(#3595: SIGSEGV in
RailMonitor::updateBestMapping()after peer restart,caused by a use-after-free of a cached remote
Topologypointer). SetMC_USE_TENT=1in the pod environment and re-ran the identicalreproduction from step (2).
What we found (3)
No effect. The crash signature stayed in
real_client.cpp, identical tobefore. Either this wheel's TENT transport is not wired to that
flag, needs a different value, or is not compiled into this release at all.
We could not confirm which, since we only have the compiled wheel, not the
source. This is a ruled-out fix attempt, not a working one.
Our hypothesis for issue 1 (not confirmed against source)
The pattern — retries exhausted, then a crash referencing a "not
initialized" client state — closely matches #3595's description of a stale
reference surviving a peer-topology change and being dereferenced on the
failure-handling path without revalidation. If
real_client.cppholds asimilar cached pointer/reference to peer or segment state that becomes
invalid when a peer joins or leaves, and the retry-exhaustion path in this
file doesn't revalidate it before use, that would explain this exact
signature.
Issue 2: silent Put failure (TRANSFER_FAIL), no crash
What we did (4): store data, then check for errors beyond the success log
On an already-stable pool (no crash in progress), stored a prefix on one pod
and read that pod's complete log output for the request, not just the
single "Stored" confirmation line.
What we found (4)
The LMCache-level log line reports success regardless:
But immediately following it, in the same pod's log:
This failure is never surfaced anywhere above the Transfer Engine layer.
The calling LMCache integration only checks whether the local CPU-tier
store succeeded — it does not check whether the remote Mooncake
Putsucceeded, so a failed remote write is reported identically to a successful
one. A later
Getfor this same data, from a different pod, then correctlyreports a miss — not because the master forgot good data, but because the
data was never actually written to the shared tier in the first place.
Full context from one reproduction, showing the Put failure immediately
preceded by a cascade of RDMA handshake failures against devices with no
real network attachment in our environment (expected/known — see
Environment above):
With
transfer_timeout: 1(1 second) in our config, cycling through 5-7known-broken devices before falling back to the one working device does not
always complete in time.
What we did (5): test a candidate fix (reduce device_name to the one working device)
Hypothesizing the timeout was the issue, redeployed with
device_namereduced from the full 8-device list to just
"mlx5_0"— the one device thatactually works — removing all the wasted retry cycles entirely. Re-ran the
identical store/retrieve reproduction from step (4).
What we found (5)
Did not fix it — and surfaced a different, more concerning signature for
the same
TRANSFER_FAIL:localandpeerare the identical address (<pod-ip>:14082@mlx5_0on both sides) — the engine is attempting to establish an RDMA connection to
itself. We separately observed a similarly-shaped self-collision under
tensor-parallel (TP=4) workers sharing one pod (distinct workers registering
distinct clients, causing address collisions) — but this reproduction used
TP=1, a single worker, with only one device configured, so whatever
produces this self-referential peer address is not specific to multi-worker
tensor parallelism, and reducing the device list does not avoid it. This is
a ruled-out fix attempt, not a working one — we reverted back to the full
8-device list, which carries no additional risk beyond the already-documented
handshake retries.
Our hypothesis for issue 2 (not confirmed against source)
Given both issues produce connection errors referencing stale or
self-referential peer/endpoint state, specifically following a change in
pool membership or device configuration, we suspect they may share a root
cause in how the Transfer Engine selects and tracks peer connection state —
but we have not confirmed this against source, only against observed
behavior, since we only have the compiled wheel.
What does mitigate (not fix) issue 1
Not a code question, included for completeness since it shaped how
seriously we treat this: issue 1's crash is a one-time, self-healing event
per membership-change — the affected pod recovers automatically in 10-15
seconds. Running at least 3 client pods, replacing them one at a time
(never all at once), kept at least 2 pods continuously healthy and serving
real traffic through 3 full replacement cycles in our testing, including
one cycle where the crash genuinely fired. This does not reduce how often
the crash happens — it only ensures redundant capacity survives it. Issue 2
has no equivalent mitigation we could find; it is silent by nature.
Suggested next step
Given the structural similarity to #3595, the most likely fix for issue 1 is
the same one applied there, ported to
real_client.cpp: hold any cachedpeer/segment reference the failure-handling path touches as a
shared_ptr/revalidated reference rather than a raw pointer that can outliveits backing object across a membership change, or re-validate it on the
retry-exhaustion path before use. Issue 2's self-referential peer address
(step 5's finding) suggests a related bug in peer/endpoint selection that
may be worth investigating together with issue 1, though we have not
confirmed they share a code path. Happy to test a patched build against the
exact reproductions above if one becomes available.
Found while validating a production KV-cache deployment plan. Happy to share more raw logs or test a patched build against the exact reproductions above.
@github-actions[bot] commented on GitHub (Oct 13, 2026):
Thanks for opening this issue, @sivabreddy!
113816799A maintainer will triage this when possible. To help us respond faster, please include:
Useful links: Documentation · Contributing guide
@sivabreddy commented on GitHub (Oct 13, 2026):
Follow-up: retested with a corrected RDMA device list and checked TENT directly from source
Posting an update since we found something that changes the picture from the original report above.
1. Is TENT available as a way around this? No — confirmed from source, not guessed
We'd read about TENT (the newer failure-handling transport) and wondered if it was reachable from
the published package. Cloned
kvcache-ai/Mooncakefresh and checked directly:mooncake-common/common.cmake:option(USE_TENT "option for building Mooncake TENT" OFF)—TENT is a CMake build-time flag, default OFF, not a runtime environment variable.
MC_USE_TENT=1attempt (mentioned in the original report) had noeffect — there is no such runtime flag in this transport. The published
mooncake-transfer-enginePyPI wheel is a standard build (USE_TENTnot set), so TENT is notpresent in it regardless of environment variables.
2. Our original device-list mismatch, corrected — the crash got worse, not better
The original report used a
device_namelist of 8 devices, of which only 1 (mlx5_0) actuallyhad a routable RoCEv2 identity on our cluster — we wondered if this mismatch itself was a
contributing factor. Re-checked fresh: still true, no change, confirmed via
ibv_devinfoand ascan of
/sys/class/infiniband/*/ports/1/gids/*on both nodes.Redeployed with
device_namecorrected to"mlx5_0"only (the one real device). Result:All three client pods failed to establish a working Mooncake RDMA client at startup — 20/20
connection retries exhausted on every pod, 0/3 successful:
A separate issue found here: the Mooncake master's own metrics showed
Clients: 0for theentire test, even though every pod logged
MooncakeConnector initialized successfullyon theLMCache side. LMCache's
MooncakestoreConnectorappears to report success regardless of whetherthe underlying C++ client actually has a working connection — there's no visible warning anywhere
that the connector is non-functional. (We realize this specific piece may belong on the LMCache
side rather than here, flagging for completeness since it compounds the diagnosis difficulty.)
The crash itself still reproduced, and more readily than before — not gated behind a
membership-change event anymore, but firing on literally the first real cache operation:
Confirmed independently on 2 of the 3 pods (identical file/line numbers, identical trigger: a
lookup reporting a partial local hit that needs to load the remainder from the remote tier). One
pod was also force-deleted and replaced to confirm the original membership-change trigger
specifically — same result, crashes immediately on the replacement pod's first real request.
Total: 5 independent crash observations across 3 pods, 100% reproduction rate, 0 successful real
cache operations observed anywhere in this retest.
3. What this changes about our read of the original report
one device that was letting any connection succeed at all in our original 8-device config,
without touching whatever is fragile in
real_client.cpp's client-creation/reconnection path.config, since the crash now pre-empts it entirely (the engine dies before a store can ever
complete) — not evidence that bug is fixed, just that this config's failure mode is more severe
and reaches a different point first.
Happy to share the full reproduction (manifests, complete logs) if useful, or test against a
patched build if one becomes available.
@xiaodouzi666 commented on GitHub (Oct 19, 2026):
Hi, I'll post some fix measures later today, if you have any questions about that please do not hesitate to leave a comment, thx!
@sivabreddy commented on GitHub (Oct 19, 2026):
Update after a re-test: the failed writes came from our RDMA configuration; the crash is the null-client path that #4633 fixes.
On 2026-10-17 we re-tested with the same versions: vLLM 0.17.1, LMCache 0.4.1 (
LMCacheConnectorV1,mooncakestore://), Mooncake 0.3.13.post1 and Llama-3.1-8B TP1. We used the same LMCache YAML and the HTTP metadata server, with 3 pods on 2 nodes. Several RDMA pods on each node share one RoCE device. Differences from the original run: an Ubuntu CUDA image with pip, and LMCache 0.4.1 built from source.1. What was wrong on our side
MC_GID_INDEX=3. A cluster policy also injectedNCCL_IB_GID_INDEX=3, which Mooncake uses whenMC_GID_INDEXis not set (mooncake-transfer-engine/src/config.cppL144-145). When pods share one RDMA device, each pod also sees the GID entries of the other pods. So a fixed index points to at most one pod's own GID, or to another pod's GID. Without a fixed index,findBestGidIndexranks GIDs that have a readable sysfs netdev first, and each pod selected its own IPv4 GID (indexes 11–27).device_name. We listed 8 devices, and only one has a network.2. Re-test with automatic GID selection
Failed to create clientretriesPut ... TRANSFER_FAILlog lines (4 pods)PutEnd)Store-only test (Python client, 4 clients, 2 per node, 16 keys of 4 MB each, P2PHANDSHAKE, connected device only): with
MC_GID_INDEX=3, 16 of 16 puts failed on every client and 0 of 64 keys were readable; with automatic selection, 64 of 64 keys were readable.3. The crash: #4633 matches our logs
In our crash logs the process first fails to create the client (
Failed to create client ... retry 20/20) or logsClient is not initialized; in one log the last Mooncake line before the segfault isreal_client.cpp:3154] Client is not initialized(batch_get_buffer_internal, 0.3.13.post1). In 0.3.13.post1,RealClient::batch_get_buffer(L3366-3373) passes an always-true success predicate toexecute_timed_operation, so the observer callsclient_->ObserveTransferOperation(...)with a nullclient_. The same pattern is in the other 7 batch wrappers (L4024, 4086, 4377, 4656, 5435, 6053, 6097) and is still onmain. This is exactly what #4633 fixes (thanks @pjdurden). So the chain in our case was: fixed GID index → client creation fails → LMCache keeps calling the batch API (it does not check thesetup()result) → nullclient_in the metrics observer → segfault. With automatic GID selection the client is created and the crash path is not reached, which is why the re-test had 0 crashes. We did not re-run the crash setup with #4633 applied.4. What we suggest
docs/source/design/transfer-engine/index.mdL473 says the default ofMC_GID_INDEXis 3. In the code the default is -1 = automatic selection (mooncake-transfer-engine/include/config.hL46). Proposed text: "MC_GID_INDEX(orNCCL_IB_GID_INDEXwhenMC_GID_INDEXis not set): GID index per device. Default: automatic selection, which prefers a GID that has a network device. Do not set a fixed index when several containers share one RDMA device: the GID table holds entries for the interfaces of all containers on the node." We can send this as a PR.MC_GID_INDEX/NCCL_IB_GID_INDEX, and list only RDMA devices that have a network indevice_name.Stored ...after a failed Mooncake Put, and continues after a failedsetup()) is on the LMCache side; we will raise it there. We withdraw the #3595 hypothesis.@xiaodouzi666 thank you for the offer. #4633 already covers the crash; feel free to use or improve anything above. The membership change itself was not the cause.
We will close this issue once #4633 (or an equivalent) is merged, unless you prefer to close it earlier.
@xiaodouzi666 commented on GitHub (Oct 26, 2026):
Hi, I believe this crash has been solved, in order to solve that, I implemented the fix under these two PRs (#4932, #4933), please take a look, and I also added the test procedure inside, I believe this could work as well in your device, however, I'm still not so clear about what caused your setup failure, if you could share more details that would be even better. If there's any other problem, plz let me know :))
@sivabreddy commented on GitHub (Oct 26, 2026):
Thank you @xiaodouzi666 for #4932 and #4933. I tested both today.
Why
setup()failed on our side (details in my comment of 2026-10-19 above): we had setMC_GID_INDEX=3(and a cluster policy injectedNCCL_IB_GID_INDEX=3) while several pods on each node share one RoCE device, so index 3 did not point to the pod's own GID, and we listed 8 RDMA devices indevice_namewhere only one has a network. Client creation then failed on all 20 retries. With automatic GID selection and only the connected device, the client is created and we had no crash.Test of #4932 + #4933. Two wheels built with the same flags (
-DUSE_CUDA=OFF -DWITH_STORE=ON -DWITH_TE=ON, Python 3.10, Ubuntu 22.04, x86_64):mainatac4d29bf(merge base) and #4933 head69c49020(includes #4932). For each case and each API, a new Python process callsMooncakeDistributedStore.setup(...)and then one batch API with dummy buffers.tcp, master127.0.0.1:1): the path of this issue ("Client is not initialized").rdma,mlx5_99).setup()(S1 and S2)mainbatch_get_buffer[None, None]get_into_ranges[[[-600]]]batch_put_from[-600]batch_get_into[-600]batch_get_into_multi_buffers[-600]batch_upsert_from[-600][-600]batch_put_from_multi_buffers[][]So 5 of the 7 batch APIs in the Python binding crash on
mainin both cases, and none crash with the PRs. A correct setup (local master,tcp) givessetup() == 0and a workingput/getwith both builds.#4933 also works as described: the retry line now names the reason (
Failed to create client on port 13897: RPC_FAIL, retry 1/20in S1,...: INTERNAL_ERROR, retry 1/20in S2), and in S1setup()returns-900(RPC_FAIL) instead of-1. In our original logs only the retry line was visible, so this would have shown the cause at once.Two small notes:
batch_put_from_multi_buffersreturns an empty list, not one error per key (same onmain), so a caller cannot see which keys failed. Not related to the crash.I will close this issue when one of the fixes is merged, unless you prefer to close it earlier.