kvcache-ai / kvcache-ai/Mooncake

[TENT] RDMA link-flap recovery gaps: hung WRs wedge QPs silently, swallowed bootstrap rejection, no PORT_ACTIVE fallback

Open
#3,523 1 comment 0 reactions 0 assignees View on GitHub
Dominant language
C++
Stars
6.6k
Forks
1.2k
Avg merge
3d 5h
Merged PRs (30d)
312

Description

# [TENT] RDMA link-flap recovery gaps: hung WRs wedge QPs silently (no error WC, no software timeout, no reconnect), plus swallowed bootstrap rejection and missing PORT_ACTIVE fallback

## Scenario

Prefill/Decode disaggregation (KV cache transfer) over RoCE with the TENT backend
(`MC_USE_TENT=1`), RDMA NICs are bonded (`mlx5_bond_*`).

**Observed behavior:** after PD transfer timeouts, or an RDMA NIC link down/up,
subsequent transfers never recover on their own. The classic (non-TENT) engine does
not show this behavior in the same environment.

## Controlled experiment (single-host repro)

`ip link set down`, wait ~4 min, `ip link set up`.

Timeline (all from logs):

- `08:18:06` — link down; all 8 engine processes correctly log `Action: mlx5_bond_4 down`.
- `08:19:03-08:19:54` — `fallback device selection` floods (~780k reroute attempts);
throughput degrades but does not drop to zero (other NICs still serve).
- `08:19:35+` — application-level 20 s timeouts (`batch_transfer_sync_write TIMEOUT`,
ret=-1).
- `08:21:54` — link up; all 8 processes correctly log `Action: mlx5_bond_4 up`
(so PORT_ACTIVE handling worked in this run).
- After link up: **partial recovery only**. All 4 pp_ranks submitted RDMA writes
(25 each), but pp_rank 1/2 succeeded 25/25 while **pp_rank 0/3 failed 25/25** —
and stayed broken until the end of the log (16+ min later).

The crucial part: for the failing ranks after link-up, the TENT layer logged
**absolutely nothing**:

- 0 × `Detected error WQE` (no error completions)
- 0 × `transfer timeout (software)` (slice-level software timeout never fired)
- 0 × `Unable to connect endpoint` (no reconnect attempts/failures)
- 0 × `Endpoint marked for destruction`

Slices were submitted at the app level and then vanished: never completed, never
errored, never reported by any TENT diagnostic path.

## Root cause analysis

### Defect A (primary, confirmed by experiment): hung WRs during link-down wedge the QP/endpoint forever

WRs posted to QPs on the downed NIC hang silently: while the port is down the
hardware neither transmits nor completes/errors them. After the link comes back,
those WRs **still never complete and never produce an error CQE**, so:

1. The QP's send-depth accounting (`wr_depth` / `cq_outstanding`) stays consumed
forever;
2. New slices posted to that endpoint either cannot be posted (QP full) or queue
behind dead WRs and are never sent;
3. Because these slices never reach the posted/completed/error paths, **none of
TENT's detection hooks fire**: no error WC (so no `disableEndpoint`), and the
software slice timeout never triggers for them, so the endpoint is never
destroyed and rebuilt;
4. The endpoint becomes a permanent blackhole. In our experiment the two ranks
whose paths crossed the downed NIC never recovered (16+ min, until shutdown),
while ranks on other NICs recovered immediately.

There is currently **no mechanism** in TENT that reclaims an endpoint with
outstanding WRs after a link recovery (e.g. destroying endpoints with stale
outstanding WRs on `PORT_ACTIVE`), and no visibility into stuck posted WRs
(the classic engine gained exactly this diagnostic in #2872 via
`MC_TRACK_RDMA_POSTED_SLICES`; TENT has nothing equivalent).

### Defect B: bootstrap RPC swallows the peer's rejection reason → misleading "Missing peer GID in bootstrap"

`ControlService::onBootstrapRdma` (`tent/src/runtime/control_plane.cpp`) ignores the
return value of `bootstrap_callback_`. When the peer rejects the bootstrap (stale
endpoint retired, device down, ...), the real reason goes into
`local_desc.reply_msg`, but the RPC still returns success with an empty `local_gid`,
and the initiator reports the misleading `Missing peer GID in bootstrap`
(`tent/src/transport/rdma/endpoint.cpp`). The classic engine fixed the equivalent
issue in #2959 (structured handshake rejection replies); TENT still has it at
v0.3.12.

### Defect C (latent, not triggered in this run): `PORT_ACTIVE` can be lost → context stuck in DEVICE_PAUSED forever

The async fd is registered with `EPOLLET` (`tent/src/transport/rdma/context.cpp`),
but `Workers::monitorThread` does `epoll_wait(..., maxevents=1, 100)` and
`handleContextEvents` reads exactly one `ibv_async_event` per wakeup
(`tent/src/transport/rdma/workers.cpp`). Under edge-triggered semantics, when
multiple events queue (e.g. a fast flap), `PORT_ACTIVE` may never be processed.
`resume()` has no other caller, and there is no periodic `ibv_query_port`
validation, so a context can stay `DEVICE_PAUSED` forever (making
`Workers::getEndpoint` fail all transfers on that local NIC permanently). The
classic engine is saved from this by its monitor worker unconditionally
re-activating contexts every second; TENT has no such fallback. In our controlled
experiment (down ~4 min, single events far apart) this race did not trigger, but
it remains a latent risk for fast flaps.

### Defect D: GID/LID cached at init and never refreshed

`RdmaContext` reads `gid_`/`lid_` once in `openDevice()` and always advertises these
cached values in bootstrap. If the GID changes after a flap while the process keeps
running, peers build AH entries from stale address vectors. The classic engine fixed
this in #2878 (refresh on HCA/GID change events); TENT still caches forever at
v0.3.12.

## Related classic-engine fixes (none of them touch TENT)

- #2872 — RDMA rail-failure handling + CQ timeout diagnostics (`MC_TRACK_RDMA_POSTED_SLICES`)
- #2959 — RDMA NIC failover recovery: local-RNIC handoff, `MC_RDMA_RAIL_PAUSE_SECONDS`,
structured handshake rejection replies
- #2878 — refresh RDMA metadata on HCA and GID change events
- #2941 — pause active reconnects to failed peers

Verified at tag v0.3.12: `tent/src/runtime/control_plane.cpp` and
`tent/src/transport/rdma/workers.cpp` still contain defects A–D.

## Suggested fixes for TENT

1. On `PORT_ACTIVE` (context resume), proactively destroy endpoints that still have
outstanding WRs instead of waiting forever for CQEs that will never arrive;
more generally, give an endpoint with stuck outstanding WRs a deadline after
which it is torn down and rebuilt.
2. Port the #2872-style posted-slice tracking / CQ-timeout diagnostics to TENT so
"slice posted but never completed" is visible.
3. Extend the software slice timeout to cover slices sitting in the per-worker
request queues waiting to be posted, not only posted inflight slices.
4. Propagate bootstrap failures through the control RPC (carry `reply_msg` or an
error status back to the initiator).
5. Drain the async event fd until `ibv_get_async_event` returns empty on each epoll
wakeup, and add a periodic `ibv_query_port`-based resume fallback.
6. Refresh the cached GID/LID on port/GID change events, like #2878 does for the
classic engine.

Happy to provide full logs or help test patches.

Contributor guide

Open the contributing guide

Research direction

Run the documented single-host link-flap reproduction, then read tent/src/runtime/control_plane.cpp, tent/src/transport/rdma/context.cpp, tent/src/transport/rdma/workers.cpp, and tent/src/transport/rdma/endpoint.cpp. Trace endpoint recovery, async-event handling, bootstrap replies, and cached GID/LID use against the classic fixes cited in #2872, #2959, and #2878. Done means link flaps recover transfers, bootstrap rejection reasons reach the initiator, and fast PORT_ACTIVE events do not leave contexts paused.

Written by the indexing model from the issue text.

Assessment

Tech stack
cpp
Domain
distributed-systems, networking
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
38/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.